mod benches;
mod check;
mod doc;
mod format;
mod languageserver;
mod loader;
mod run;
mod tests;
mod visitor;
mod naming;
use std::fmt;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use rust_alloc::string::String;
use rust_alloc::vec::Vec;
use crate::alloc;
use crate::alloc::prelude::*;
use crate::workspace::{self, WorkspaceFilter};
use anyhow::{bail, Context as _, Error, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::filter::EnvFilter;
use crate::compile::{ItemBuf, ParseOptionError};
use crate::modules::capture_io::CaptureIo;
use crate::termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
use crate::{Context, ContextError, Options, Hash};
const DEFAULT_ABOUT: &str = "The Rune Language Interpreter";
#[non_exhaustive]
pub struct ContextOptions<'a> {
pub capture: Option<&'a CaptureIo>,
pub experimental: bool,
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(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.as_str() {
"always" => ColorChoice::Always,
"ansi" => ColorChoice::AlwaysAnsi,
"auto" => {
if atty::is(atty::Stream::Stdout) {
ColorChoice::Auto
} else {
ColorChoice::Never
}
}
"never" => ColorChoice::Never,
_ => ColorChoice::Auto,
};
let mut stdout = StandardStream::stdout(choice);
let mut stderr = StandardStream::stderr(choice);
let mut io = Io {
stdout: &mut stdout,
stderr: &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 mut o = io.stdout.lock();
o.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
let result = format_errors(&mut o, &error);
o.set_color(&ColorSpec::new())?;
result?;
Ok(ExitCode::Failure)
}
}
}
}
pub(crate) enum EntryPoint<'a> {
Path(PathBuf),
Package(workspace::FoundPackage<'a>),
}
impl EntryPoint<'_> {
pub(crate) fn path(&self) -> &Path {
match self {
EntryPoint::Path(path) => path,
EntryPoint::Package(p) => &p.found.path,
}
}
}
struct Io<'a> {
stdout: &'a mut StandardStream,
stderr: &'a mut StandardStream,
}
#[derive(Parser, Debug, Clone)]
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::default();
if self.command.is_debug() {
options.debug_info(true);
options.test(true);
options.bytecode(false);
}
for option in &self.shared.compiler_options {
options.parse_option(option)?;
}
Ok(options)
}
}
#[derive(Clone, Copy)]
struct CommandSharedRef<'a> {
shared: &'a SharedFlags,
command: &'a dyn CommandBase,
}
impl CommandSharedRef<'_> {
fn find_bins(&self) -> Option<WorkspaceFilter<'_>> {
if !self.command.is_workspace(AssetKind::Bin) {
return None;
}
Some(if let Some(name) = &self.shared.bin {
WorkspaceFilter::Name(name)
} else {
WorkspaceFilter::All
})
}
fn find_tests(&self) -> Option<WorkspaceFilter<'_>> {
if !self.command.is_workspace(AssetKind::Test) {
return None;
}
Some(if let Some(name) = &self.shared.test {
WorkspaceFilter::Name(name)
} else {
WorkspaceFilter::All
})
}
fn find_examples(&self) -> Option<WorkspaceFilter<'_>> {
if !self.command.is_workspace(AssetKind::Bin) {
return None;
}
Some(if let Some(name) = &self.shared.example {
WorkspaceFilter::Name(name)
} else {
WorkspaceFilter::All
})
}
fn find_benches(&self) -> Option<WorkspaceFilter<'_>> {
if !self.command.is_workspace(AssetKind::Bench) {
return None;
}
Some(if let Some(name) = &self.shared.bench {
WorkspaceFilter::Name(name)
} else {
WorkspaceFilter::All
})
}
}
#[derive(Parser, Debug)]
struct HashFlags {
#[arg(long)]
random: bool,
#[arg(name = "item")]
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) {
}
}
#[derive(Subcommand, Debug)]
enum Command {
Check(CommandShared<check::Flags>),
Doc(CommandShared<doc::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; 8] = [
"check",
"doc",
"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::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::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),
Package(workspace::FoundPackage<'a>),
}
#[derive(Default)]
struct Config {
manifest: workspace::Manifest,
test: bool,
verbose: bool,
manifest_root: Option<PathBuf>,
found_paths: alloc::Vec<PathBuf>,
}
impl Config {
fn build_paths<'m>(&'m self, cmd: CommandSharedRef<'_>) -> 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| BuildPath::Path(p)))?;
if !cmd.shared.workspace {
return Ok(build_paths);
}
}
if let Some(bin) = cmd.find_bins() {
for p in self.manifest.find_bins(bin)? {
build_paths.try_push(BuildPath::Package(p))?;
}
}
if let Some(test) = cmd.find_tests() {
for p in self.manifest.find_tests(test)? {
build_paths.try_push(BuildPath::Package(p))?;
}
}
if let Some(example) = cmd.find_examples() {
for p in self.manifest.find_examples(example)? {
build_paths.try_push(BuildPath::Package(p))?;
}
}
if let Some(bench) = cmd.find_benches() {
for p in self.manifest.find_benches(bench)? {
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,
experimental: self.experimental,
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(Parser, Debug)]
#[command(name = "rune", about = None)]
struct Args {
#[arg(long)]
version: bool,
#[arg(short = 'C', long, default_value = "auto")]
color: String,
#[command(subcommand)]
cmd: Option<Command>,
}
#[derive(Parser, Debug, Clone)]
struct SharedFlags {
#[arg(long)]
experimental: bool,
#[arg(long, short = 'R')]
recursive: bool,
#[arg(long)]
warnings: bool,
#[arg(long)]
verbose: bool,
#[arg(long)]
workspace: bool,
#[arg(name = "option", short = 'O', number_of_values = 1)]
compiler_options: Vec<String>,
#[arg(long = "bin")]
bin: Option<String>,
#[arg(long = "test")]
test: Option<String>,
#[arg(long = "example")]
example: Option<String>,
#[arg(long = "bench")]
bench: Option<String>,
#[arg(name = "paths")]
paths: Vec<PathBuf>,
}
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>(mut o: O, error: &Error) -> io::Result<()>
where
O: 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, cmd: CommandSharedRef<'_>) -> Result<()> {
c.found_paths.try_extend(
cmd.shared
.paths
.iter()
.map(|p| p.as_path().into()),
)?;
if !c.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() {
c.found_paths.try_push(path.into())?;
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)?;
c.manifest = result?;
Ok(())
}
async fn main_with_out(io: &mut Io<'_>, entry: &mut Entry<'_>, mut args: Args) -> Result<ExitCode> {
let mut c = Config::default();
if let Some((shared, base)) = args.cmd.as_mut().and_then(|c| c.as_command_base_mut()) {
base.propagate(&mut c, shared);
}
let cmd = match &args.cmd {
Some(cmd) => cmd,
None => {
let commands: alloc::String = Command::ALL.into_iter().try_join(", ")?;
writeln!(io.stdout, "Expected a subcommand: {commands}")?;
return Ok(ExitCode::Failure);
}
};
let mut entrys = alloc::Vec::new();
if let Some(cmd) = cmd.as_command_shared_ref() {
populate_config(io, &mut c, cmd)?;
let build_paths = c.build_paths(cmd)?;
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) => {
for path in loader::recurse_paths(recursive, path.try_to_owned()?) {
entrys.try_push(EntryPoint::Path(path?))?;
}
}
BuildPath::Package(p) => {
if verbose {
let mut o = io.stderr.lock();
o.set_color(ColorSpec::new().set_fg(Some(Color::Green)).set_bold(true))?;
let result = write!(o, "{:>12}", what);
o.set_color(&ColorSpec::new())?;
o.flush()?;
result?;
writeln!(o, " {} `{}` (from {})", p.found.kind, p.found.path.display(), p.package.name)?;
}
entrys.try_push(EntryPoint::Package(p))?;
}
}
}
}
match run_path(io, &c, cmd, entry, entrys).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 {
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::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 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 load = loader::load(
io,
&context,
&f.shared,
&options,
e.path(),
visitor::Attribute::None,
)?;
match run::run(io, c, &f.command, &context, load.unit, &load.sources).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 {
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)
}