val 0.3.6

An arbitrary precision calculator language
Documentation
use super::*;

#[derive(Clap, Debug)]
#[clap(
  about,
  author,
  version,
  help_template = "\
{before-help}{name} {version}
{author}
{about}

\x1b[1;4mUsage\x1b[0m: {usage}

{all-args}{after-help}
"
)]
pub struct Arguments {
  #[clap(
    short,
    long,
    conflicts_with = "filename",
    help = "Expression to evaluate"
  )]
  expression: Option<String>,

  #[clap(conflicts_with = "expression", help = "File to evaluate")]
  filename: Option<PathBuf>,

  #[clap(
    short,
    long,
    conflicts_with = "filename",
    help = "Load files before entering the REPL"
  )]
  load: Option<Vec<PathBuf>>,

  #[clap(
    short,
    long,
    default_value = "1024",
    help = "Decimal precision to use for calculations"
  )]
  precision: usize,

  #[clap(
    short,
    long,
    value_parser = clap::value_parser!(RoundingMode),
    default_value = "to-even",
    help = "Rounding mode to use for calculations",
  )]
  rounding_mode: RoundingMode,

  #[clap(
    long,
    default_value = "128",
    help = "Stack size in MB for evaluations"
  )]
  pub stack_size: usize,
}

impl Arguments {
  pub fn run(self) -> Result {
    match (&self.filename, &self.expression) {
      (Some(filename), _) => self.eval(filename.clone()),
      (_, Some(expression)) => self.eval_expression(expression.clone()),
      _ => {
        #[cfg(not(target_family = "wasm"))]
        {
          self.read()
        }
        #[cfg(target_family = "wasm")]
        {
          Err(anyhow::anyhow!("Interactive mode not supported in WASM"))
        }
      }
    }
  }

  fn eval(&self, filename: PathBuf) -> Result {
    let content = fs::read_to_string(&filename)?;

    let filename = filename.to_string_lossy().to_string();

    let mut evaluator = Evaluator::from(Environment::new(Config {
      precision: self.precision,
      rounding_mode: self.rounding_mode.into(),
    }));

    match parse(&content) {
      Ok(ast) => match evaluator.eval(&ast) {
        Ok(_) => Ok(()),
        Err(error) => {
          error
            .report(&filename)
            .eprint((filename.as_str(), Source::from(content)))?;

          process::exit(1);
        }
      },
      Err(errors) => {
        for error in errors {
          error
            .report(&filename)
            .eprint((filename.as_str(), Source::from(&content)))?;
        }

        process::exit(1);
      }
    }
  }

  fn eval_expression(&self, value: String) -> Result {
    let mut evaluator = Evaluator::from(Environment::new(Config {
      precision: self.precision,
      rounding_mode: self.rounding_mode.into(),
    }));

    match parse(&value) {
      Ok(ast) => match evaluator.eval(&ast) {
        Ok(value) => {
          if let Value::Null = value {
            return Ok(());
          }

          println!("{}", value);

          Ok(())
        }
        Err(error) => {
          error
            .report("<expression>")
            .eprint(("<expression>", Source::from(value)))?;

          process::exit(1);
        }
      },
      Err(errors) => {
        for error in errors {
          error
            .report("<expression>")
            .eprint(("<expression>", Source::from(&value)))?;
        }

        process::exit(1);
      }
    }
  }

  #[cfg(not(target_family = "wasm"))]
  fn read(&self) -> Result {
    let history = dirs::home_dir().unwrap_or_default().join(".val_history");

    let editor_config = Builder::new()
      .color_mode(ColorMode::Enabled)
      .edit_mode(EditMode::Emacs)
      .history_ignore_space(true)
      .completion_type(CompletionType::Circular)
      .max_history_size(1000)?
      .build();

    let mut editor =
      Editor::<Highlighter, DefaultHistory>::with_config(editor_config)?;

    editor.set_helper(Some(Highlighter::new()));
    editor.load_history(&history).ok();

    let mut evaluator = Evaluator::from(Environment::new(Config {
      precision: self.precision,
      rounding_mode: self.rounding_mode.into(),
    }));

    if let Some(filenames) = &self.load {
      for filename in filenames {
        let content: &'static str =
          Box::leak(fs::read_to_string(filename)?.into_boxed_str());

        let filename = filename.to_string_lossy().to_string();

        match parse(content) {
          Ok(ast) => match evaluator.eval(&ast) {
            Ok(_) => {}
            Err(error) => {
              error
                .report(&filename)
                .eprint((filename.as_str(), Source::from(content)))?;

              process::exit(1);
            }
          },
          Err(errors) => {
            for error in errors {
              error
                .report(&filename)
                .eprint((filename.as_str(), Source::from(&content)))?;
            }

            process::exit(1);
          }
        }
      }
    }

    loop {
      let line = editor.readline("> ")?;

      editor.add_history_entry(&line)?;
      editor.save_history(&history)?;

      let line: &'static str = Box::leak(line.into_boxed_str());

      match parse(line) {
        Ok(ast) => match evaluator.eval(&ast) {
          Ok(value) if !matches!(value, Value::Null) => println!("{value}"),
          Ok(_) => {}
          Err(error) => error
            .report("<input>")
            .eprint(("<input>", Source::from(line)))?,
        },
        Err(errors) => {
          for error in errors {
            error
              .report("<input>")
              .eprint(("<input>", Source::from(line)))?;
          }
        }
      }
    }
  }
}

#[cfg(test)]
mod tests {
  use {super::*, clap::Parser, std::path::PathBuf};

  #[test]
  fn filename_only() {
    let arguments = Arguments::parse_from(vec!["program", "file.txt"]);

    assert!(arguments.filename.is_some());
    assert!(arguments.expression.is_none());

    assert_eq!(arguments.filename.unwrap(), PathBuf::from("file.txt"));
  }

  #[test]
  fn expression_only() {
    let arguments =
      Arguments::parse_from(vec!["program", "--expression", "1 + 2"]);

    assert!(arguments.filename.is_none());
    assert!(arguments.expression.is_some());

    assert_eq!(arguments.expression.unwrap(), "1 + 2");
  }

  #[test]
  fn expression_short_form() {
    let arguments = Arguments::parse_from(vec!["program", "-e", "1 + 2"]);

    assert!(arguments.filename.is_none());
    assert!(arguments.expression.is_some());

    assert_eq!(arguments.expression.unwrap(), "1 + 2");
  }

  #[test]
  fn both_should_fail() {
    assert!(
      Arguments::try_parse_from(vec![
        "program",
        "file.txt",
        "--expression",
        "1 + 2"
      ])
      .is_err()
    );
  }

  #[test]
  fn neither_provided() {
    let arguments = Arguments::parse_from(vec!["program"]);

    assert!(arguments.filename.is_none());
    assert!(arguments.expression.is_none());
  }

  #[test]
  fn conflict_error_message() {
    let result = Arguments::try_parse_from(vec![
      "program",
      "file.txt",
      "--expression",
      "1 + 2",
    ]);

    assert!(result.is_err());

    let error = result.unwrap_err().to_string();

    assert!(
      error.contains("cannot be used with"),
      "Error should mention conflicts: {}",
      error
    );
  }

  #[test]
  fn load_conflicts_with_filename() {
    let result = Arguments::try_parse_from(vec![
      "program",
      "file.txt",
      "--load",
      "prelude.val",
    ]);

    assert!(result.is_err(), "Parser should reject filename + --load");

    let error = result.unwrap_err().to_string();

    assert!(
      error.contains("cannot be used with"),
      "Error should mention conflicts: {}",
      error
    );
  }
}