mod ace;
mod benches;
mod check;
mod doc;
mod format;
mod languageserver;
mod loader;
mod naming;
mod out;
mod run;
mod tests;
mod visitor;
use std::fmt;
use std::io::{self, IsTerminal, Write};
use std::path::{Path, PathBuf};
use rust_alloc::string::String;
use rust_alloc::vec::Vec;
use crate as rune;
use crate::alloc;
use crate::alloc::prelude::*;
use crate::workspace::{self, WorkspaceFilter};
use anyhow::{bail, Context as _, Error, Result};
use clap::{Parser, Subcommand, ValueEnum};
use tracing_subscriber::filter::EnvFilter;
use crate::compile::ParseOptionError;
use crate::modules::capture_io::CaptureIo;
use crate::termcolor::{ColorChoice, StandardStream};
use crate::{Context, ContextError, Hash, ItemBuf, Options};
use self::out::{Color, Io, Stream};
const DEFAULT_ABOUT: &str = "The Rune Language Interpreter";
#[non_exhaustive]
pub struct ContextOptions<'a> {
pub capture: Option<&'a CaptureIo>,
pub test: bool,
}
pub type ContextBuilder = dyn FnMut(ContextOptions<'_>) -> Result<Context, ContextError>;
#[derive(Default)]
pub struct Entry<'a> {
about: Option<alloc::String>,
context: Option<&'a mut ContextBuilder>,
}
impl<'a> Entry<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn about(mut self, about: impl fmt::Display) -> Self {
self.about = Some(
about
.try_to_string()
.expect("Failed to format about string"),
);
self
}
pub fn context(mut self, context: &'a mut ContextBuilder) -> Self {
self.context = Some(context);
self
}
pub fn run(self) -> ! {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("Failed to build runtime");
match runtime.block_on(self.inner()) {
Ok(exit_code) => {
std::process::exit(exit_code as i32);
}
Err(error) => {
let o = std::io::stderr();
let _ = format_errors(&mut o.lock(), &error);
std::process::exit(ExitCode::Failure as i32);
}
}
}
pub async fn run_async(self) -> ! {
match self.inner().await {
Ok(exit_code) => {
std::process::exit(exit_code as i32);
}
Err(error) => {
let o = std::io::stderr();
let _ = format_errors(&mut o.lock(), &error);
std::process::exit(ExitCode::Failure as i32);
}
}
}
async fn inner(mut self) -> Result<ExitCode> {
let args = match Args::try_parse() {
Ok(args) => args,
Err(e) => {
let about = self.about.as_deref().unwrap_or(DEFAULT_ABOUT);
let code = if e.use_stderr() {
let o = std::io::stderr();
let mut o = o.lock();
o.write_all(about.as_bytes())?;
writeln!(o)?;
writeln!(o)?;
writeln!(o, "{}", e)?;
o.flush()?;
ExitCode::Failure
} else {
let o = std::io::stdout();
let mut o = o.lock();
o.write_all(about.as_bytes())?;
writeln!(o)?;
writeln!(o)?;
writeln!(o, "{}", e)?;
o.flush()?;
ExitCode::Success
};
return Ok(code);
}
};
if args.version {
let o = std::io::stdout();
let mut o = o.lock();
let about = self.about.as_deref().unwrap_or(DEFAULT_ABOUT);
o.write_all(about.as_bytes())?;
o.flush()?;
return Ok(ExitCode::Success);
}
let choice = match args.color {
ColorArgument::Always => ColorChoice::Always,
ColorArgument::Ansi => ColorChoice::AlwaysAnsi,
ColorArgument::Auto => {
if std::io::stdin().is_terminal() {
ColorChoice::Auto
} else {
ColorChoice::Never
}
}
ColorArgument::Never => ColorChoice::Never,
};
let mut stdout = StandardStream::stdout(choice);
let mut stderr = StandardStream::stderr(choice);
let mut io = Io::new(&mut stdout, &mut stderr);
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.init();
match main_with_out(&mut io, &mut self, args).await {
Ok(code) => Ok(code),
Err(error) => {
let o = io.with_color(Stream::Stdout, Color::Error)?;
format_errors(o, &error)?;
o.close()?;
Ok(ExitCode::Failure)
}
}
}
}
#[derive(TryClone)]
pub(crate) enum EntryPoint<'a> {
Path(PathBuf, bool),
Package(workspace::FoundPackage<'a>),
}
impl EntryPoint<'_> {
pub(crate) fn path(&self) -> &Path {
match self {
EntryPoint::Path(path, _) => path,
EntryPoint::Package(p) => &p.found.path,
}
}
pub(crate) fn is_argument(&self) -> bool {
match self {
EntryPoint::Path(_, explicit) => *explicit,
EntryPoint::Package(..) => false,
}
}
}
impl fmt::Display for EntryPoint<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
EntryPoint::Path(path, false) => {
write!(f, "path in {}", path.display())
}
EntryPoint::Path(path, true) => {
write!(f, "path in {} (argument)", path.display())
}
EntryPoint::Package(package) => {
write!(
f,
"package `{}` in {} ({})",
package.package.name,
package.found.path.display(),
package.found.kind
)
}
}
}
}
#[derive(Parser, Debug, Clone)]
#[command(rename_all = "kebab-case")]
struct CommandShared<T>
where
T: clap::Args,
{
#[command(flatten)]
shared: SharedFlags,
#[command(flatten)]
command: T,
}
impl<T> CommandShared<T>
where
T: CommandBase + clap::Args,
{
fn options(&self) -> Result<Options, ParseOptionError> {
let mut options = Options::from_default_env()?;
if self.command.is_debug() {
options.debug_info(true);
options.test(true);
options.bytecode(false);
}
for option in &self.shared.compiler_option {
options.parse_option(option)?;
}
Ok(options)
}
}
#[derive(Clone, Copy)]
struct CommandSharedRef<'a> {
shared: &'a SharedFlags,
command: &'a dyn CommandBase,
}
impl<'a> CommandSharedRef<'a> {
fn find(
&self,
all_targets: bool,
kind: AssetKind,
name: Option<&'a str>,
) -> Option<WorkspaceFilter<'a>> {
if !all_targets && !self.command.is_workspace(kind) {
return None;
}
if let Some(name) = name {
return Some(WorkspaceFilter::Name(name));
}
self.shared.is_unfiltered().then_some(WorkspaceFilter::All)
}
#[inline]
fn find_bins(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
self.find(all_targets, AssetKind::Bin, self.shared.bin.as_deref())
}
#[inline]
fn find_tests(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
self.find(all_targets, AssetKind::Test, self.shared.test.as_deref())
}
#[inline]
fn find_examples(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
self.find(all_targets, AssetKind::Bin, self.shared.example.as_deref())
}
#[inline]
fn find_benches(&self, all_targets: bool) -> Option<WorkspaceFilter<'a>> {
self.find(all_targets, AssetKind::Bench, self.shared.bench.as_deref())
}
}
#[derive(Parser, Debug)]
#[command(rename_all = "kebab-case")]
struct HashFlags {
#[arg(long)]
random: bool,
#[arg(long)]
count: Option<usize>,
item: Vec<String>,
}
enum AssetKind {
Bin,
Test,
Bench,
}
trait CommandBase {
#[inline]
fn is_debug(&self) -> bool {
false
}
#[inline]
fn is_workspace(&self, _: AssetKind) -> bool {
false
}
#[inline]
fn describe(&self) -> &str {
"Running"
}
#[inline]
fn propagate(&mut self, _: &mut Config, _: &mut SharedFlags) {}
#[inline]
fn paths(&self) -> &[PathBuf] {
&[]
}
}
#[derive(Subcommand, Debug)]
enum Command {
Check(CommandShared<check::Flags>),
Doc(CommandShared<doc::Flags>),
Ace(CommandShared<ace::Flags>),
Test(CommandShared<tests::Flags>),
Bench(CommandShared<benches::Flags>),
Run(CommandShared<run::Flags>),
Fmt(CommandShared<format::Flags>),
LanguageServer(SharedFlags),
Hash(HashFlags),
}
impl Command {
const ALL: [&'static str; 9] = [
"check",
"doc",
"ace",
"test",
"bench",
"run",
"fmt",
"languageserver",
"hash",
];
fn as_command_base_mut(&mut self) -> Option<(&mut SharedFlags, &mut dyn CommandBase)> {
let (shared, command): (_, &mut dyn CommandBase) = match self {
Command::Check(shared) => (&mut shared.shared, &mut shared.command),
Command::Doc(shared) => (&mut shared.shared, &mut shared.command),
Command::Ace(shared) => (&mut shared.shared, &mut shared.command),
Command::Test(shared) => (&mut shared.shared, &mut shared.command),
Command::Bench(shared) => (&mut shared.shared, &mut shared.command),
Command::Run(shared) => (&mut shared.shared, &mut shared.command),
Command::Fmt(shared) => (&mut shared.shared, &mut shared.command),
Command::LanguageServer(..) => return None,
Command::Hash(..) => return None,
};
Some((shared, command))
}
fn as_command_shared_ref(&self) -> Option<CommandSharedRef<'_>> {
let (shared, command): (_, &dyn CommandBase) = match self {
Command::Check(shared) => (&shared.shared, &shared.command),
Command::Doc(shared) => (&shared.shared, &shared.command),
Command::Ace(shared) => (&shared.shared, &shared.command),
Command::Test(shared) => (&shared.shared, &shared.command),
Command::Bench(shared) => (&shared.shared, &shared.command),
Command::Run(shared) => (&shared.shared, &shared.command),
Command::Fmt(shared) => (&shared.shared, &shared.command),
Command::LanguageServer(..) => return None,
Command::Hash(..) => return None,
};
Some(CommandSharedRef { shared, command })
}
}
enum BuildPath<'a> {
Path(&'a Path, bool),
Package(workspace::FoundPackage<'a>),
}
#[derive(Default)]
struct Config {
filtered: bool,
test: bool,
verbose: bool,
all_targets: bool,
manifest_root: Option<PathBuf>,
}
#[derive(Default)]
struct Inputs {
manifest: workspace::Manifest,
found_paths: alloc::Vec<(PathBuf, bool)>,
}
impl Inputs {
fn build_paths<'m>(
&'m self,
cmd: CommandSharedRef<'_>,
c: &mut Config,
) -> Result<alloc::Vec<BuildPath<'m>>> {
let mut build_paths = alloc::Vec::new();
if !self.found_paths.is_empty() {
build_paths.try_extend(self.found_paths.iter().map(|(p, e)| BuildPath::Path(p, *e)))?;
if !cmd.shared.workspace {
return Ok(build_paths);
}
}
if let Some(filter) = cmd.find_bins(c.all_targets) {
c.filtered |= !matches!(filter, WorkspaceFilter::All);
for p in self.manifest.find_bins(filter)? {
build_paths.try_push(BuildPath::Package(p))?;
}
}
if let Some(filter) = cmd.find_tests(c.all_targets) {
c.filtered |= !matches!(filter, WorkspaceFilter::All);
for p in self.manifest.find_tests(filter)? {
build_paths.try_push(BuildPath::Package(p))?;
}
}
if let Some(filter) = cmd.find_examples(c.all_targets) {
c.filtered |= !matches!(filter, WorkspaceFilter::All);
for p in self.manifest.find_examples(filter)? {
build_paths.try_push(BuildPath::Package(p))?;
}
}
if let Some(filter) = cmd.find_benches(c.all_targets) {
c.filtered |= !matches!(filter, WorkspaceFilter::All);
for p in self.manifest.find_benches(filter)? {
build_paths.try_push(BuildPath::Package(p))?;
}
}
Ok(build_paths)
}
}
impl SharedFlags {
fn context(
&self,
entry: &mut Entry<'_>,
c: &Config,
capture: Option<&CaptureIo>,
) -> Result<Context> {
let opts = ContextOptions {
capture,
test: c.test,
};
let mut context =
entry
.context
.as_mut()
.context("Context builder not configured with Entry::context")?(opts)?;
if let Some(capture) = capture {
context.install(crate::modules::capture_io::module(capture)?)?;
}
Ok(context)
}
}
#[derive(Default, Debug, Clone, Copy, ValueEnum)]
enum ColorArgument {
#[default]
Auto,
Ansi,
Always,
Never,
}
#[derive(Parser, Debug)]
#[command(name = "rune", about = None)]
struct Args {
#[arg(long)]
version: bool,
#[arg(short = 'C', long, default_value = "auto")]
color: ColorArgument,
#[command(subcommand)]
cmd: Option<Command>,
}
#[derive(Parser, Debug, Clone)]
#[command(rename_all = "kebab-case")]
struct SharedFlags {
#[arg(long, short = 'R')]
recursive: bool,
#[arg(long)]
warnings: bool,
#[arg(long)]
verbose: bool,
#[arg(long)]
workspace: bool,
#[arg(short = 'O', num_args = 1)]
compiler_option: Vec<String>,
#[arg(long)]
list_options: bool,
#[arg(long)]
bin: Option<String>,
#[arg(long)]
test: Option<String>,
#[arg(long)]
example: Option<String>,
#[arg(long)]
bench: Option<String>,
#[arg(long)]
all_targets: bool,
#[arg(long)]
path: Vec<PathBuf>,
}
impl SharedFlags {
fn is_unfiltered(&self) -> bool {
self.bin.is_none()
&& self.test.is_none()
&& self.example.is_none()
&& self.bench.is_none()
&& self.path.is_empty()
}
}
const SPECIAL_FILES: &[&str] = &[
"main.rn",
"lib.rn",
"src/main.rn",
"src/lib.rn",
"script/main.rn",
"script/lib.rn",
];
#[repr(i32)]
enum ExitCode {
Success = 0,
Failure = 1,
VmError = 2,
}
fn format_errors<O>(o: &mut O, error: &Error) -> io::Result<()>
where
O: ?Sized + io::Write,
{
writeln!(o, "Error: {}", error)?;
for error in error.chain().skip(1) {
writeln!(o, "Caused by: {}", error)?;
}
Ok(())
}
fn find_manifest() -> Option<(PathBuf, PathBuf)> {
let mut path = PathBuf::new();
loop {
let manifest_path = path.join(workspace::MANIFEST_FILE);
if manifest_path.is_file() {
return Some((path, manifest_path));
}
path.push("..");
if !path.is_dir() {
return None;
}
}
}
fn populate_config(
io: &mut Io<'_>,
c: &mut Config,
inputs: &mut Inputs,
cmd: CommandSharedRef<'_>,
) -> Result<()> {
c.all_targets = cmd.shared.all_targets;
inputs
.found_paths
.try_extend(cmd.shared.path.iter().map(|p| (p.clone(), false)))?;
inputs
.found_paths
.try_extend(cmd.command.paths().iter().map(|p| (p.clone(), true)))?;
if !inputs.found_paths.is_empty() && !cmd.shared.workspace {
return Ok(());
}
let Some((manifest_root, manifest_path)) = find_manifest() else {
for file in SPECIAL_FILES {
let path = Path::new(file);
if path.is_file() {
inputs.found_paths.try_push((path.try_to_owned()?, false))?;
return Ok(());
}
}
let special = SPECIAL_FILES.join(", ");
bail!(
"Could not find `{}` in this or parent directories nor any of the special files: {special}",
workspace::MANIFEST_FILE
)
};
c.verbose = true;
c.manifest_root = Some(manifest_root);
let mut sources = crate::Sources::new();
sources.insert(crate::Source::from_path(manifest_path)?)?;
let mut diagnostics = workspace::Diagnostics::new();
let result = workspace::prepare(&mut sources)
.with_diagnostics(&mut diagnostics)
.build();
diagnostics.emit(io.stdout, &sources)?;
inputs.manifest = result?;
Ok(())
}
async fn main_with_out(io: &mut Io<'_>, entry: &mut Entry<'_>, mut args: Args) -> Result<ExitCode> {
let mut c = Config::default();
let mut inputs = Inputs::default();
if let Some((shared, base)) = args.cmd.as_mut().and_then(|c| c.as_command_base_mut()) {
base.propagate(&mut c, shared);
}
let Some(cmd) = &args.cmd else {
let commands: alloc::String = Command::ALL.into_iter().try_join(", ")?;
writeln!(io.stdout, "Expected a subcommand: {commands}")?;
return Ok(ExitCode::Failure);
};
let mut entries = alloc::Vec::new();
if let Some(cmd) = cmd.as_command_shared_ref() {
if cmd.shared.list_options {
writeln!(
io.stdout,
"Available compiler options (set with -O <option>=<value>):"
)?;
writeln!(io.stdout)?;
for (i, option) in Options::available().iter().enumerate() {
if i > 0 {
writeln!(io.stdout)?;
}
io.write(
format_args!("{}", option.key),
Stream::Stdout,
Color::Highlight,
)?;
write!(io.stdout, "={}", option.default)?;
if option.unstable {
io.write(" (unstable)", Stream::Stdout, Color::Error)?;
}
writeln!(io.stdout, ":")?;
writeln!(io.stdout, " Options: {}", option.options)?;
writeln!(io.stdout)?;
for &line in option.doc {
let line = line.strip_prefix(' ').unwrap_or(line);
writeln!(io.stdout, " {line}")?;
}
}
return Ok(ExitCode::Success);
}
populate_config(io, &mut c, &mut inputs, cmd)?;
let build_paths = inputs.build_paths(cmd, &mut c)?;
let what = cmd.command.describe();
let verbose = c.verbose;
let recursive = cmd.shared.recursive;
for build_path in build_paths {
match build_path {
BuildPath::Path(path, explicit) => {
for path in loader::recurse_paths(recursive, path.try_to_owned()?) {
entries.try_push(EntryPoint::Path(path?, explicit))?;
}
}
BuildPath::Package(p) => {
if verbose {
let mut section = io.section(what, Stream::Stderr, Color::Highlight)?;
section.append(format_args!(
" {} `{}` (from {})",
p.found.kind,
p.found.path.display(),
p.package.name
))?;
section.close()?;
}
entries.try_push(EntryPoint::Package(p))?;
}
}
}
}
match run_path(io, &c, cmd, entry, entries).await? {
ExitCode::Success => (),
other => {
return Ok(other);
}
}
Ok(ExitCode::Success)
}
async fn run_path<'p, I>(
io: &mut Io<'_>,
c: &Config,
cmd: &Command,
entry: &mut Entry<'_>,
entries: I,
) -> Result<ExitCode>
where
I: IntoIterator<Item = EntryPoint<'p>>,
{
match cmd {
Command::Check(f) => {
let options = f.options()?;
for e in entries {
let mut options = options.clone();
if e.is_argument() {
options.function_body = true;
}
match check::run(io, entry, c, &f.command, &f.shared, &options, e.path())? {
ExitCode::Success => (),
other => return Ok(other),
}
}
}
Command::Doc(f) => {
let options = f.options()?;
return doc::run(io, entry, c, &f.command, &f.shared, &options, entries);
}
Command::Ace(f) => {
let options = f.options()?;
return ace::run(io, entry, c, &f.command, &f.shared, &options, entries);
}
Command::Fmt(f) => {
let options = f.options()?;
return format::run(io, entry, c, entries, &f.command, &f.shared, &options);
}
Command::Test(f) => {
let options = f.options()?;
match tests::run(io, c, &f.command, &f.shared, &options, entry, entries).await? {
ExitCode::Success => (),
other => return Ok(other),
}
}
Command::Bench(f) => {
let options = f.options()?;
for e in entries {
let mut options = options.clone();
if e.is_argument() {
options.function_body = true;
}
let capture_io = crate::modules::capture_io::CaptureIo::new();
let context = f.shared.context(entry, c, Some(&capture_io))?;
let load = loader::load(
io,
&context,
&f.shared,
&options,
e.path(),
visitor::Attribute::Bench,
)?;
match benches::run(
io,
&f.command,
&context,
Some(&capture_io),
load.unit,
&load.sources,
&load.functions,
)
.await?
{
ExitCode::Success => (),
other => return Ok(other),
}
}
}
Command::Run(f) => {
let options = f.options()?;
let context = f.shared.context(entry, c, None)?;
for e in entries {
let mut options = options.clone();
if e.is_argument() {
options.function_body = true;
}
let load = loader::load(
io,
&context,
&f.shared,
&options,
e.path(),
visitor::Attribute::None,
)?;
let entry = if e.is_argument() {
Hash::EMPTY
} else {
Hash::type_hash(["main"])
};
match run::run(io, c, &f.command, &context, load.unit, &load.sources, entry).await?
{
ExitCode::Success => (),
other => return Ok(other),
}
}
}
Command::LanguageServer(shared) => {
let context = shared.context(entry, c, None)?;
languageserver::run(context).await?;
}
Command::Hash(args) => {
use rand::prelude::*;
if args.random {
for _ in 0..args.count.unwrap_or(1) {
let mut rand = rand::thread_rng();
writeln!(io.stdout, "{}", Hash::new(rand.gen::<u64>()))?;
}
}
for item in &args.item {
let item: ItemBuf = item.parse()?;
let hash = Hash::type_hash(&item);
writeln!(io.stdout, "{item} => {hash}")?;
}
}
}
Ok(ExitCode::Success)
}