vimlrs 0.2.1

Faithful Rust port of the Vimscript (VimL) interpreter, from the Neovim C eval engine
Documentation
//! `viml` command-line driver (the `vimlrs` crate's binary).
//!
//! Modes:
//! - `viml --expr 'EXPR'` — evaluate one expression, print its value.
//! - `viml --cmd 'echo …'` — run one ex command line.
//! - `viml FILE.vim` — source a script file.
//! - `viml` (no args) — read-eval-print loop on stdin.

use std::io::{self, BufRead, IsTerminal, Write};
use std::path::PathBuf;
use std::process::ExitCode;

use clap::{CommandFactory, FromArgMatches, Parser};

use crate::aot;
use crate::fusevm_bridge::{eval_expr, eval_file, eval_source};
use crate::ported::eval::encode::encode_tv2echo;
use crate::ported::message::did_emsg;
use crate::script_cache;
use crate::viml_lexer::VimlError;

// ── MenkeTechnologies house `--help` (cyberpunk style; see `tp -h`) ──────────
// ANSI-Shadow "VIMLRS" wordmark glyphs live in `crate::banner::LOGO` (single
// source of truth shared with the REPL/stats banner) — cyan → magenta → red.

/// clap help template: yellow `USAGE:` + cyan section rules around the green-`//`
/// option and positional lists, with the banner/footer supplied at runtime.
const HELP_TEMPLATE: &str = "\n{before-help}\n{about}\n\n\x1b[33m  USAGE:\x1b[0m {usage}\n\n\x1b[36m  ── EVALUATION ─────────────────────────────────────\x1b[0m\n{options}\n\x1b[36m  ── INPUT ──────────────────────────────────────────\x1b[0m\n{positionals}\n{after-help}";

/// Banner + a status box padded at runtime so its right border never drifts as
/// the version grows, closed by the magenta `>> … <<` tagline.
fn banner() -> String {
    const BOX_W: usize = 50;
    let ver = env!("CARGO_PKG_VERSION");
    let l = crate::banner::LOGO;
    let (b1, b2, b3, b4, b5, b6) = (l[0], l[1], l[2], l[3], l[4], l[5]);
    let status = format!(" STATUS: ONLINE  // SIGNAL: ████████░░ // v{ver}");
    let space = " ".repeat(BOX_W.saturating_sub(status.chars().count()));
    let rule = "─".repeat(BOX_W);
    format!(
        "\n\x1b[36m {b1}\x1b[0m\n\x1b[36m {b2}\x1b[0m\n\x1b[35m {b3}\x1b[0m\n\x1b[35m {b4}\x1b[0m\n\x1b[31m {b5}\x1b[0m\n\x1b[31m {b6}\x1b[0m\n \x1b[36m┌{rule}┐\x1b[0m\n \x1b[36m│\x1b[0m{status}{space}\x1b[36m│\x1b[0m\n \x1b[36m└{rule}┘\x1b[0m\n\x1b[35m  >> VIML INTERPRETER ON FUSEVM // FULL SPECTRUM <<\x1b[0m"
    )
}

/// SYSTEM footer: version + copyright + tagline + block rule.
fn footer() -> String {
    let ver = env!("CARGO_PKG_VERSION");
    format!(
        "\x1b[36m  ── SYSTEM ─────────────────────────────────────────\x1b[0m\n  \x1b[35mv{ver} \x1b[0m// \x1b[33m(c) MenkeTechnologies\x1b[0m\n  \x1b[35mThe script is compiled. The runtime is vast.\x1b[0m\n  \x1b[32m//\x1b[0m run \x1b[36mviml --repl\x1b[0m (or bare \x1b[36mviml\x1b[0m in a terminal) for the interactive REPL\n  \x1b[33m>>> JACK IN. SOURCE THE SCRIPT. RUN VIML EVERYWHERE. <<<\x1b[0m\n \x1b[36m░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░\x1b[0m"
    )
}

/// Parsed command line.
#[derive(Parser, Debug)]
#[command(
    name = "viml",
    version,
    about = "VimL (Vimscript) interpreter on the fusevm bytecode VM"
)]
pub struct Cli {
    /// Evaluate a single expression and print its value.
    #[arg(
        short = 'e',
        long = "expr",
        value_name = "EXPR",
        help = "\x1b[32m//\x1b[0m Evaluate one expression and print its value"
    )]
    expr: Option<String>,

    /// Execute a single ex command line (e.g. 'echo 1 + 1').
    #[arg(
        short = 'c',
        long = "cmd",
        value_name = "CMD",
        help = "\x1b[32m//\x1b[0m Run one ex command line (e.g. 'echo 1 + 1')"
    )]
    cmd: Option<String>,

    /// AOT build: bake the given script files into a self-contained executable
    /// at this path.
    #[arg(
        short = 'b',
        long = "build",
        value_name = "OUT",
        help = "\x1b[32m//\x1b[0m Bake the script file(s) into a self-contained executable at OUT"
    )]
    build: Option<PathBuf>,

    /// With --build: AOT-compile to native machine code (Cranelift object
    /// linked standalone) instead of embedding the source.
    #[arg(
        short = 'n',
        long = "native",
        help = "\x1b[32m//\x1b[0m With --build: AOT-compile to native code (Cranelift), not embed source"
    )]
    native: bool,

    /// Delete the rkyv bytecode script cache and exit.
    #[arg(
        long = "clear-cache",
        help = "\x1b[32m//\x1b[0m Delete the rkyv bytecode script cache and exit"
    )]
    clear_cache: bool,

    /// Run the Language Server Protocol server on stdio (for editors).
    #[arg(
        long = "lsp",
        help = "\x1b[32m//\x1b[0m Run the Language Server Protocol server on stdio (for editors)"
    )]
    lsp: bool,

    /// Run the Debug Adapter Protocol server on stdio (for editors).
    #[arg(
        long = "dap",
        help = "\x1b[32m//\x1b[0m Run the Debug Adapter Protocol server on stdio (for editors)"
    )]
    dap: bool,

    /// Print the fusevm bytecode listing before running.
    #[arg(
        long = "disasm",
        help = "\x1b[32m//\x1b[0m Print the fusevm bytecode listing before running"
    )]
    disasm: bool,

    /// Force the interactive reedline REPL (banner, live stats, Tab completion,
    /// history). Implied when stdin is a terminal and no other mode is given.
    #[arg(
        long = "repl",
        help = "\x1b[32m//\x1b[0m Force the interactive REPL (banner, Tab completion, history)"
    )]
    pub repl: bool,

    /// VimL script file(s) to source (cached). With --build, the inputs to bake.
    #[arg(
        value_name = "FILE",
        help = "\x1b[32m//\x1b[0m VimL script file(s) to source; with --build, the inputs to bake"
    )]
    files: Vec<PathBuf>,
}

/// Whether any error was raised in the last run (`did_emsg` set).
fn had_error() -> bool {
    did_emsg.with(|d| d.get()) != 0
}

fn exit_for_errors() -> ExitCode {
    if had_error() {
        ExitCode::FAILURE
    } else {
        ExitCode::SUCCESS
    }
}

/// Parse args and dispatch. Returns the process exit code.
pub fn run() -> ExitCode {
    // Render `--help`/`-h` in the MenkeTechnologies house style: augment the
    // derived command with the banner, the runtime-padded status box, the
    // cyan section rules, and the SYSTEM footer (see `tp -h`).
    let cmd = Cli::command()
        .help_template(HELP_TEMPLATE)
        .before_help(banner())
        .after_help(footer());
    let cli = match Cli::from_arg_matches(&cmd.get_matches()) {
        Ok(c) => c,
        Err(e) => e.exit(),
    };

    if cli.lsp {
        return match crate::lsp::run_stdio() {
            Ok(()) => ExitCode::SUCCESS,
            Err(e) => {
                eprintln!("viml: lsp: {e}");
                ExitCode::FAILURE
            }
        };
    }

    if cli.dap {
        return match crate::dap::run_stdio() {
            Ok(()) => ExitCode::SUCCESS,
            Err(e) => {
                eprintln!("viml: dap: {e}");
                ExitCode::FAILURE
            }
        };
    }

    crate::fusevm_disasm::set_enabled(cli.disasm);

    if cli.clear_cache {
        if let Some(cache) = script_cache::CACHE.as_ref() {
            if let Err(e) = cache.clear() {
                eprintln!("viml: clear-cache: {e}");
                return ExitCode::FAILURE;
            }
        }
        return ExitCode::SUCCESS;
    }

    if let Some(out) = cli.build {
        let result = if cli.native {
            aot::build_native(&cli.files, &out)
        } else {
            aot::build(&cli.files, &out)
        };
        return match result {
            Ok(p) => {
                println!("{}", p.display());
                ExitCode::SUCCESS
            }
            Err(e) => {
                eprintln!("{e}");
                ExitCode::FAILURE
            }
        };
    }

    if let Some(src) = cli.expr {
        return match eval_expr(&src) {
            Ok(v) => {
                // As with :echo, a runtime error during evaluation means there is
                // no valid result to print — emit only the error (already on
                // stderr), not a spurious fallback value.
                if !had_error() {
                    println!("{}", encode_tv2echo(&v));
                }
                exit_for_errors()
            }
            Err(e) => fail(e),
        };
    }

    if let Some(src) = cli.cmd {
        return match eval_source(&src) {
            Ok(_) => exit_for_errors(),
            Err(e) => fail(e),
        };
    }

    if !cli.files.is_empty() {
        // Seed the global argument list (argv()/argc()) with the file args, the
        // standalone counterpart of Vim's file arglist, before sourcing.
        let arglist: Vec<String> = cli
            .files
            .iter()
            .map(|p| p.to_string_lossy().into_owned())
            .collect();
        crate::ported::eval::funcs::set_arglist(&arglist);
        // Source each file in order through the bytecode cache.
        for path in &cli.files {
            if let Err(e) = eval_file(path) {
                return fail(e);
            }
        }
        return exit_for_errors();
    }

    // No mode selected: run the interactive reedline REPL when asked (`--repl`)
    // or when stdin is a terminal. It MUST run inline on this (worker) thread —
    // interpreter globals are thread-local, so spawning eval elsewhere would
    // reset state between turns. When stdin is a pipe/file, fall back to the
    // line-oriented `repl()` below (no reedline, no TTY control sequences).
    if cli.repl || io::stdin().is_terminal() {
        crate::repl::run()
    } else {
        repl()
    }
}

/// Report a parse/compile error (runtime `emsg`s already printed to stderr).
fn fail(e: VimlError) -> ExitCode {
    eprintln!("{e}");
    ExitCode::FAILURE
}

/// Read-eval-print loop: each input line is one statement; a bare expression
/// prints its value, an `:echo`/`:let` runs for effect.
fn repl() -> ExitCode {
    let stdin = io::stdin();
    let mut out = io::stdout();
    let prompt = |out: &mut io::Stdout| {
        let _ = write!(out, "viml> ");
        let _ = out.flush();
    };
    prompt(&mut out);
    for line in stdin.lock().lines() {
        let line = match line {
            Ok(l) => l,
            Err(_) => break,
        };
        if line.trim().is_empty() {
            prompt(&mut out);
            continue;
        }
        match eval_source(&line) {
            Ok(Some(v)) => println!("{}", encode_tv2echo(&v)),
            Ok(None) => {}
            Err(e) => eprintln!("{e}"),
        }
        prompt(&mut out);
    }
    println!();
    ExitCode::SUCCESS
}