glowdust 0.0.1

A DBMS with a data model based on functions and pattern matching
Documentation
use crate::ExecutionResult::{CompilationError, ExecutionError, ExecutionOk};
use crate::GlowdustCommand::{Exit, GdString, Log, Reset, Unknown};

use glowdust::compiler::parser::lexer::TokenStream;
use glowdust::runtime::vm::VirtualMachine;
use std::io::Error;
use std::process;

use glowdust::compiler::parser::ast_walker::walk;
use glowdust::compiler::parser::parse_state::ParseState;
use glowdust::compiler::parser::parser::Parser;
use glowdust::compiler::parser::CompileError;
use glowdust::compiler::value::parameters::Variable;
use glowdust::runtime::sink::PrintingSink;
use glowdust::store::memory::MemoryStore;
use rustyline::error::ReadlineError;
use rustyline::history::DefaultHistory;
use rustyline::validate::MatchingBracketValidator;
use rustyline::Editor;
use rustyline::Result;
use rustyline::{Completer, Helper, Highlighter, Hinter, Validator};
use tracing::warn;
use tracing_subscriber::filter::Filtered;
use tracing_subscriber::fmt::Layer;
use tracing_subscriber::reload::Handle;
use tracing_subscriber::{fmt, prelude::*, reload, EnvFilter, Registry};

struct GlowdustData {
    vm: VirtualMachine,
    state: ParseState,
    store: MemoryStore,
    log_reloader: Handle<Filtered<Layer<Registry>, EnvFilter, Registry>, Registry>,
    buffer: String,
    global_variables: Vec<Variable>,
}

enum ExecutionResult {
    ExecutionOk,
    CompilationError(Box<Vec<CompileError>>),
    ExecutionError(String),
}

enum GlowdustCommand {
    GdString,
    Reset, // forget all variables and constants - everything but functions in store
    Exit,
    // TODO
    //Show, // stack, variables, function names, function values, function definitions
    // :show functions -> show function names
    // :show function value book -> show function book values

    // :show function definition book -> show function book definitions in a numbered list
    // :show function definition book <nr> -> dump disassembly of definition

    // :show stack -> dump current stack
    // :show variables -> show all top level variables with values
    Log,
    Unknown,
}

struct GlowdustExecutableCommand {
    command: GlowdustCommand,
    arguments: Option<String>,
}

impl GlowdustExecutableCommand {
    fn execute(&self, state: &mut GlowdustData) -> ExecutionResult {
        match self.command {
            GdString => {
                let mut parser = Parser::new(TokenStream::from(&state.buffer));
                // TODO the whole state of affairs with local/global variables and scope is just depressing.
                state.state.script_symbols.variables = state.global_variables.clone();
                parser.parse(&mut state.state);
                if !state.state.errors.is_empty() {
                    return CompilationError(state.state.errors.clone());
                }

                let compiler_result = walk(&state.state, &mut state.store);
                if let Err(err) = compiler_result {
                    return ExecutionError(err.to_string());
                }
                let executable = compiler_result.unwrap();
                let sink = &mut PrintingSink::default();
                let run_result = state.vm.run(&executable, &mut state.store, sink);
                if let Err(err) = run_result {
                    return ExecutionError(err.to_string());
                }
                // TODO this is just horrible, need a better interface to consolidate new with old state
                // println!("script variables are {:?}", executable.script_variables);
                for possibly_new_var in executable.script_variables {
                    let mut found = false;
                    for existing_var in &state.global_variables {
                        if existing_var.name == possibly_new_var.name {
                            found = true;
                            break;
                        }
                    }
                    if !found {
                        state.global_variables.push(possibly_new_var);
                    }
                }
            }
            Log => match &self.arguments {
                None => {}
                Some(arg) => {
                    match match arg.trim() {
                        "trace" => state.log_reloader.modify(|layer| {
                            *layer.filter_mut() = EnvFilter::new("rustyline=ERROR,glowdust=TRACE")
                        }),
                        "debug" => state.log_reloader.modify(|layer| {
                            *layer.filter_mut() = EnvFilter::new("rustyline=ERROR,glowdust=DEBUG")
                        }),
                        "warn" => state.log_reloader.modify(|layer| {
                            *layer.filter_mut() = EnvFilter::new("rustyline=ERROR,glowdust=WARN")
                        }),
                        _ => {
                            return ExecutionError(
                                "Valid log levels are trace and debug".to_string(),
                            );
                        }
                    } {
                        Ok(_) => {
                            println!("Changed level ok");
                        }
                        Err(_) => {
                            println!("Changing level didn't work at all");
                        }
                    }
                }
            },
            Reset => {
                state.state = ParseState::default();
            }
            Exit => {
                process::exit(0);
            }
            Unknown => {
                let mut message = String::from("Unknown command: ");
                match &self.arguments {
                    Some(unknown_command) => {
                        message.push_str(unknown_command);
                    }
                    None => message.push_str("could not parse input"),
                }
                message.push_str(". Valid commands are :show, :log, :exit and :reset");
                warn!("{message}");
            }
        }
        ExecutionOk
    }
}

fn exit() -> ! {
    process::exit(0);
}

#[derive(Completer, Helper, Highlighter, Hinter, Validator)]
struct InputValidator {
    #[rustyline(Validator)]
    brackets: MatchingBracketValidator,
}

fn handle_input(
    editor: &mut Editor<InputValidator, DefaultHistory>,
    data: &mut GlowdustData,
) -> Result<GlowdustExecutableCommand> {
    let readline = editor.readline(">> ");
    match readline {
        Ok(line) => {
            editor.add_history_entry(&line)?;
            data.buffer = line.clone();
            Ok(parse_command(&line))
        }
        Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
            exit();
        }
        Err(err) => Err(ReadlineError::from(Error::other(err))),
    }
}

fn parse_command(input: &str) -> GlowdustExecutableCommand {
    let trimmed = input.trim();
    return if let Some(stripped) = trimmed.strip_prefix(':') {
        match stripped.trim().split_once(' ') {
            // --------^^^^ extra trim in case a space follows ':'
            Some(("log", args)) => GlowdustExecutableCommand {
                command: Log,
                arguments: Some(args.to_string()),
            },
            Some((_, _)) => GlowdustExecutableCommand {
                command: Unknown,
                arguments: Some(trimmed.to_string()),
            },
            None => match stripped.trim() {
                "log" => GlowdustExecutableCommand {
                    command: Log,
                    arguments: Some("show".to_string()),
                },
                "exit" | "quit" => GlowdustExecutableCommand {
                    command: Exit,
                    arguments: None,
                },
                "reset" => GlowdustExecutableCommand {
                    command: Reset,
                    arguments: None,
                },
                c => GlowdustExecutableCommand {
                    command: Unknown,
                    arguments: Some(c.to_string()),
                },
            },
        }
    } else {
        GlowdustExecutableCommand {
            command: GdString,
            arguments: Some(trimmed.to_string()),
        }
    };
}

fn main() -> Result<()> {
    let env_filter = EnvFilter::new("rustyline=ERROR,glowdust=ERROR");
    let layer = fmt::layer().with_filter(env_filter);
    let (filtered_layer, reload_handle) = reload::Layer::new(layer);
    tracing_subscriber::registry().with(filtered_layer).init();

    let mut state = GlowdustData {
        vm: VirtualMachine::new(),
        state: ParseState::default(),
        store: MemoryStore::new(),
        log_reloader: reload_handle,
        buffer: String::new(),
        global_variables: vec![],
    };
    // It will also need command hints at some point, see https://docs.rs/crate/rustyline/12.0.0/source/examples/diy_hints.rs
    let h = InputValidator {
        brackets: MatchingBracketValidator::new(),
    };
    let mut rl = Editor::new().unwrap();
    rl.set_helper(Some(h));
    loop {
        let command = handle_input(&mut rl, &mut state)?;
        match command.execute(&mut state) {
            ExecutionOk => {
                // This is a hack. rustyline requires a new line print for output to appear. Not sure why.
                // And no, it's not buffering, flushing stdout doesn't help. This will have to do for now
                println!();
            }
            CompilationError(error) => {
                eprintln!("Compilation error:");
                for err in *error {
                    eprintln!("At {}: {}", err.span, err.message)
                }
            }
            ExecutionError(message) => {
                eprintln!("Encountered error {}", message);
            }
        }
        state.state.reset();
        state.vm.reset();
    }
}