lamb 0.1.0

Pure untyped lambda calculus in safe Rust.
Documentation
use std::io;
use std::io::Error as IoError;
use std::io::Write;
use std::ops::Range;

use ariadne::Color;
use ariadne::ColorGenerator;
use ariadne::Fmt;
use ariadne::Label;
use ariadne::Report;
use ariadne::ReportKind;
use ariadne::Source;

use chumsky::Stream;
use chumsky::prelude::*;

use logos::Logos;

use lamb::repl::CommandOutcome;
use lamb::repl::Repl;
use lamb::repl::lexer::Token;
use lamb::repl::parser::*;

const REPORT_KIND_INFO: ReportKind = ReportKind::Custom("Info", Color::Green);

fn main() -> Result<(), IoError> {
    let mut repl = Repl::new();
    let mut color_gen = ColorGenerator::new();

    'repl: loop {
        print!("λ> ");
        io::stdout().flush()?;

        let mut source = String::new();
        if let Err(error) = io::stdin().read_line(&mut source) {
            report_read_error(&source, error)?;
            continue;
        }
        
        let lex_result = Token::lexer(&source).spanned();
        let parse_result = filler_parser().ignore_then(command_parser())
            .then_ignore(end())
            .parse(Stream::from_iter(source.len() - 1..source.len(), lex_result));
        let command = match parse_result {
            Ok(command) => command,
            Err(errors) => {
                report_syntax_error(&source, errors, &mut color_gen)?;
                continue;
            },
        };
        
        for action in repl.exec(command) {
            match action {
                CommandOutcome::TermReduced(reduced) => {
                    report_term_reduced(&source, reduced.count)?;
                    println!("{}", reduced.term);
                },
                CommandOutcome::ReduceLimitReached(limit) => report_reduce_limit_reached(&source, limit, color_gen.next())?,
                CommandOutcome::BindAdded(name) => report_binding_added(&source, name, color_gen.next())?,
                CommandOutcome::BindOverwritten(name) => {
                    let color = color_gen.next();
                    report_binding_added(&source, &name, color)?;
                    report_binding_overwritten(&source, &name, color)?;
                },
                CommandOutcome::ReduceLimitGot(limit) => report_reduce_limit_got(&source, limit, color_gen.next())?,
                CommandOutcome::ReduceLimitSet(limit) => report_reduce_limit_set(&source, limit, color_gen.next())?,
                CommandOutcome::Exit => break 'repl,
            }
        }
    }

    Ok(())
}

fn report_read_error(source: impl AsRef<str>, error: IoError) -> Result<(), IoError> {
    Report::<Range<usize>>::build(ReportKind::Error, (), 0)
        .with_message(format!("{}", error))
        .finish()
        .print(Source::from(source))
}

fn report_syntax_error(source: impl AsRef<str>, errors: Vec<Simple<Token>>, color_gen: &mut ColorGenerator) -> Result<(), IoError> {
    errors.into_iter()
        .fold(Report::build(ReportKind::Error, (), 0)
            .with_message("Invalid syntax"), |report, error| {
                let color = color_gen.next();
                report.with_label(Label::new(into_char_span(error.span(), &source))
                    .with_message(format!("{}", error.fg(color)))
                    .with_color(color))
            })
        .finish()
        .eprint(Source::from(source))
}

fn report_term_reduced(source: impl AsRef<str>, count: usize) -> Result<(), IoError> {
    Report::<Range<usize>>::build(REPORT_KIND_INFO, (), 0)
        .with_message(format!("Reduced {} times", count.fg(Color::Green)))
        .finish()
        .print(Source::from(source))
}

fn report_reduce_limit_reached(source: impl AsRef<str>, reduce_limit: usize, color: Color) -> Result<(), IoError> {
    Report::build(ReportKind::Warning, (), 0)
        .with_message("Reduction limit reached")
        .with_label(Label::new(0..source.as_ref().chars().count() - 1)
            .with_message("possibly divergent term")
            .with_color(color))
        .with_note(format!("current reduction limit is {}", reduce_limit.fg(color)))
        .finish()
        .print(Source::from(source))
}

fn report_binding_added(source: impl AsRef<str>, name: impl AsRef<str>, color: Color) -> Result<(), IoError> {
    Report::<Range<usize>>::build(REPORT_KIND_INFO, (), 0)
        .with_message(format!("Binding {} added", name.as_ref().fg(color)))
        .finish()
        .print(Source::from(source))
}

fn report_binding_overwritten(source: impl AsRef<str>, name: impl AsRef<str>, color: Color) -> Result<(), IoError> {
    Report::<Range<usize>>::build(ReportKind::Warning, (), 0)
        .with_message(format!("Binding {} overwritten", name.as_ref().fg(color)))
        .finish()
        .print(Source::from(source))
}

fn report_reduce_limit_set(source: impl AsRef<str>, reduce_limit: usize, color: Color) -> Result<(), IoError> {
    Report::<Range<usize>>::build(REPORT_KIND_INFO, (), 0)
        .with_message(format!("Reduction limit set to {}", reduce_limit.fg(color)))
        .finish()
        .print(Source::from(source))
}

fn report_reduce_limit_got(source: impl AsRef<str>, reduce_limit: usize, color: Color) -> Result<(), IoError> {
    Report::<Range<usize>>::build(REPORT_KIND_INFO, (), 0)
        .with_message(format!("Current reduction limit is {}", reduce_limit.fg(color)))
        .finish()
        .print(Source::from(source))
}

fn into_char_span(byte_span: Range<usize>, source: impl AsRef<str>) -> Range<usize> {
    let source = source.as_ref();
    source[..byte_span.start()].chars().count()..source[..byte_span.end()].chars().count()
}