harn-cli 0.8.26

CLI for the Harn programming language — run, test, REPL, format, and lint
Documentation
use std::fs;
use std::process;

use harn_lexer::{Lexer, Token};
use harn_parser::ast_json::{
    self, AstJsonProgram, TokenJson, AST_JSON_SCHEMA_VERSION, TOKEN_JSON_SCHEMA_VERSION,
};
use harn_parser::parse_source;
use serde::Serialize;
use serde_json::json;

use crate::cli::{ParseArgs, TokensArgs};
use crate::json_envelope::{self, JsonEnvelope};

pub(crate) const PARSE_JSON_SCHEMA_VERSION: u32 = AST_JSON_SCHEMA_VERSION;
pub(crate) const TOKENS_JSON_SCHEMA_VERSION: u32 = TOKEN_JSON_SCHEMA_VERSION;

pub(crate) fn run_parse(args: &ParseArgs) -> Result<(), String> {
    let source = match fs::read_to_string(&args.path) {
        Ok(source) => source,
        Err(error) if args.json => json_error::<AstJsonProgram>(
            PARSE_JSON_SCHEMA_VERSION,
            "read_failed",
            format!("failed to read {}: {error}", args.path),
            &args.path,
        ),
        Err(error) => return Err(format!("failed to read {}: {error}", args.path)),
    };

    let program = match parse_source(&source) {
        Ok(program) => program,
        Err(error) if args.json => json_error::<AstJsonProgram>(
            PARSE_JSON_SCHEMA_VERSION,
            "parse_failed",
            error.to_string(),
            &args.path,
        ),
        Err(error) => return Err(error.to_string()),
    };

    if args.json {
        let envelope = JsonEnvelope::ok(
            PARSE_JSON_SCHEMA_VERSION,
            ast_json::program_to_json(&program),
        );
        println!("{}", json_envelope::to_string_pretty(&envelope));
    } else {
        println!("{program:#?}");
    }
    Ok(())
}

pub(crate) fn run_tokens(args: &TokensArgs) -> Result<(), String> {
    let source = match fs::read_to_string(&args.path) {
        Ok(source) => source,
        Err(error) if args.json => json_error::<Vec<TokenJson>>(
            TOKENS_JSON_SCHEMA_VERSION,
            "read_failed",
            format!("failed to read {}: {error}", args.path),
            &args.path,
        ),
        Err(error) => return Err(format!("failed to read {}: {error}", args.path)),
    };

    let mut lexer = Lexer::new(&source);
    let tokens = match lexer.tokenize_with_comments() {
        Ok(tokens) => tokens,
        Err(error) if args.json => json_error::<Vec<TokenJson>>(
            TOKENS_JSON_SCHEMA_VERSION,
            "lex_failed",
            error.to_string(),
            &args.path,
        ),
        Err(error) => return Err(error.to_string()),
    };

    if args.json {
        let envelope = JsonEnvelope::ok(
            TOKENS_JSON_SCHEMA_VERSION,
            ast_json::tokens_to_json(&source, &tokens),
        );
        println!("{}", json_envelope::to_string_pretty(&envelope));
    } else {
        for token in &tokens {
            println!("{}", format_token_line(&source, token));
        }
    }
    Ok(())
}

fn format_token_line(source: &str, token: &Token) -> String {
    let lexeme = source
        .get(token.span.start..token.span.end)
        .unwrap_or_default();
    format!(
        "{:>4}:{:<3} {:>6}..{:<6} {:<22} {:?}",
        token.span.line,
        token.span.column,
        token.span.start,
        token.span.end,
        ast_json::token_kind_name(&token.kind),
        lexeme
    )
}

fn json_error<T: Serialize>(
    schema_version: u32,
    code: &'static str,
    message: String,
    path: &str,
) -> ! {
    let envelope =
        JsonEnvelope::<T>::err(schema_version, code, message).with_details(json!({ "path": path }));
    println!("{}", json_envelope::to_string_pretty(&envelope));
    process::exit(1);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn text_token_line_includes_span_kind_and_lexeme() {
        let source = "let x = 1\n";
        let mut lexer = Lexer::new(source);
        let tokens = lexer.tokenize().expect("tokenize");

        let line = format_token_line(source, &tokens[0]);

        assert!(line.contains("1:1"));
        assert!(line.contains("0..3"));
        assert!(line.contains("Let"));
        assert!(line.contains("\"let\""));
    }
}