use crate::cargo_metadata;
use crate::child_process::ChildProcess;
use crate::code_block::CodeBlock;
use crate::code_block::CodeKind;
use crate::code_block::Segment;
use crate::code_block::UserCodeInfo;
use crate::crate_config::ExternalCrate;
use crate::errors::bail;
use crate::errors::CompilationError;
use crate::errors::Error;
use crate::errors::Span;
use crate::errors::SpannedMessage;
use crate::evcxr_internal_runtime;
use crate::item;
use crate::module::Module;
use crate::module::SoFile;
use crate::runtime;
use crate::rust_analyzer::Completions;
use crate::rust_analyzer::RustAnalyzer;
use crate::rust_analyzer::TypeName;
use crate::rust_analyzer::VariableInfo;
use crate::use_trees::Import;
use anyhow::Result;
use once_cell::sync::Lazy;
use ra_ap_ide::TextRange;
use ra_ap_syntax::ast;
use ra_ap_syntax::AstNode;
use ra_ap_syntax::SyntaxKind;
use ra_ap_syntax::SyntaxNode;
use regex::Regex;
use std::collections::HashMap;
use std::collections::HashSet;
use std::ffi::OsString;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use std::time::Instant;
pub struct EvalContext {
child_process: ChildProcess,
_tmpdir: Option<tempfile::TempDir>,
module: Module,
committed_state: ContextState,
stdout_sender: crossbeam_channel::Sender<String>,
analyzer: RustAnalyzer,
initial_config: Config,
}
#[derive(Clone, Debug)]
pub(crate) struct Config {
tmpdir: PathBuf,
pub(crate) debug_mode: bool,
preserve_vars_on_panic: bool,
output_format: String,
display_types: bool,
display_final_expression: bool,
expand_use_statements: bool,
opt_level: String,
error_fmt: &'static ErrorFormat,
pub(crate) time_passes: bool,
pub(crate) linker: String,
pub(crate) codegen_backend: Option<String>,
pub(crate) sccache: Option<PathBuf>,
pub(crate) offline_mode: bool,
pub(crate) toolchain: String,
cargo_path: PathBuf,
pub(crate) rustc_path: PathBuf,
cache_bytes: u64,
pub(crate) core_extern: OsString,
pub(crate) target: String,
pub(crate) allow_static_linking: bool,
pub(crate) build_envs: HashMap<String, String>,
subprocess_path: PathBuf,
}
#[derive(Default)]
pub(crate) struct InitConfig {
tmpdir: Option<PathBuf>,
pub(crate) init: Option<PathBuf>,
pub(crate) prelude: Option<PathBuf>,
}
impl InitConfig {
fn check_if_exists(path: &Path) -> bool {
path.join("evcxr.toml").exists()
}
fn parse_from_current_dir(path: &Path) -> Result<Self, Error> {
let mut res = InitConfig::default();
let lines = std::fs::read_to_string(path.join("evcxr.toml"))?;
let mut is_start = false;
fn modify_value(value: &str) -> Result<&str, Error> {
let res = value
.trim()
.strip_prefix('"')
.ok_or_else(|| Error::Message("Syntax is wrong".into()))?
.strip_suffix('"')
.ok_or_else(|| Error::Message("Syntax is wrong".into()))?;
Ok(res)
}
for line in lines.lines() {
if line.trim() == "[config]" {
is_start = true;
continue;
}
if !is_start {
continue;
}
if let Some((key, value)) = line.split_once('=') {
let key = key.trim();
let value = modify_value(value)?;
match key {
"tmpdir" => {
res.tmpdir = PathBuf::from(value).into();
}
"init" => {
res.init = PathBuf::from(value).into();
}
"prelude" => {
res.prelude = PathBuf::from(value).into();
}
_ => {}
}
}
}
Ok(res)
}
fn parse_from_config_dir(path: &Path) -> Result<Self, Error> {
let mut res = InitConfig::default();
let init_path = path.join("init.evcxr");
if init_path.exists() {
res.init = Some(init_path);
}
let prelude_path = path.join("prelude.rs");
if prelude_path.exists() {
res.prelude = Some(prelude_path);
}
Ok(res)
}
pub(crate) fn update(&mut self, other: Self) {
if self.tmpdir.is_none() && other.tmpdir.is_some() {
self.tmpdir = other.tmpdir;
}
if self.init.is_none() && other.init.is_some() {
self.init = other.init;
}
if self.prelude.is_none() && other.prelude.is_some() {
self.prelude = other.prelude;
}
}
pub(crate) fn parse_as_one_step() -> Result<Self, Error> {
let mut init_config = InitConfig::default();
let current_dir = std::env::current_dir()?;
if Self::check_if_exists(¤t_dir) {
init_config.update(Self::parse_from_current_dir(¤t_dir)?);
}
let config_path = crate::config_dir();
if let Some(config_path) = config_path {
init_config.update(Self::parse_from_config_dir(&config_path)?);
}
if let (None, Ok(from_env)) = (&init_config.tmpdir, std::env::var("EVCXR_TMPDIR")) {
let tmpdir_path = PathBuf::from(from_env);
init_config.tmpdir = Some(tmpdir_path);
}
Ok(init_config)
}
}
fn create_initial_config(tmpdir: PathBuf, subprocess_path: PathBuf) -> Result<Config> {
let mut config = Config::new(tmpdir, subprocess_path)?;
if !cfg!(target_os = "macos") && which::which("mold").is_ok() {
config.linker = "mold".to_owned();
} else if !cfg!(target_os = "macos") && which::which("lld").is_ok() {
config.linker = "lld".to_owned();
}
Ok(config)
}
impl Config {
pub fn new(tmpdir: PathBuf, subprocess_path: PathBuf) -> Result<Self> {
let rustc_path = default_rustc_path()?;
let core_extern = core_extern(&rustc_path)?;
let target = get_host_target(Path::new(&rustc_path))?;
Ok(Config {
tmpdir,
debug_mode: false,
preserve_vars_on_panic: true,
output_format: "{:?}".to_owned(),
display_types: false,
display_final_expression: true,
expand_use_statements: true,
opt_level: "2".to_owned(),
error_fmt: &ERROR_FORMATS[0],
time_passes: false,
linker: "system".to_owned(),
cache_bytes: 0,
sccache: None,
offline_mode: false,
toolchain: String::new(),
cargo_path: default_cargo_path()?,
rustc_path,
core_extern,
target,
allow_static_linking: true,
subprocess_path,
codegen_backend: None,
build_envs: Default::default(),
})
}
pub fn set_sccache(&mut self, enabled: bool) -> Result<(), Error> {
if enabled {
if let Ok(path) = which::which("sccache") {
self.sccache = Some(path);
} else {
bail!("Couldn't find sccache. Try running `cargo install sccache`.");
}
} else {
self.sccache = None;
}
Ok(())
}
pub fn sccache(&self) -> bool {
self.sccache.is_some()
}
pub fn set_cache_bytes(&mut self, bytes: u64) {
self.cache_bytes = bytes;
}
pub fn cache_bytes(&self) -> u64 {
self.cache_bytes
}
pub(crate) fn cargo_command(&self, command_name: &str) -> Command {
let mut command = if self.linker == "mold" {
Command::new("mold")
} else {
Command::new(&self.cargo_path)
};
if self.linker == "mold" {
command.arg("-run").arg(&self.cargo_path);
}
if self.offline_mode {
command.arg("--offline");
}
let mut rustflags = vec!["-Cprefer-dynamic".to_owned()];
if self.linker == "lld" {
rustflags.push(format!("-Clink-arg=-fuse-ld={}", self.linker));
}
if self.time_passes {
rustflags.push("-Ztime-passes".to_owned());
}
if let Some(backend) = self.codegen_backend.as_ref() {
rustflags.push(format!("-Zcodegen-backend={backend}"));
}
command
.arg(command_name)
.current_dir(self.crate_dir())
.env("CARGO_TARGET_DIR", "target")
.env("RUSTC", &self.rustc_path)
.env("RUSTFLAGS", rustflags.join(" "))
.envs(&self.build_envs)
.env(crate::module::CORE_EXTERN_ENV, &self.core_extern);
if self.cache_bytes > 0 {
command.env(crate::module::CACHE_ENABLED_ENV, "1");
command.env(
crate::module::cache::TARGET_DIR_ENV,
self.common_target_dir(),
);
}
if command_name == "build" || command_name == "check" {
command
.arg("--target")
.arg(&self.target)
.arg("--message-format=json");
}
if self.allow_static_linking && self.cache_bytes == 0 {
if let Some(sccache) = &self.sccache {
command.env("RUSTC_WRAPPER", sccache);
}
} else {
command.env("RUSTC_WRAPPER", &self.subprocess_path);
command.env(runtime::WRAP_RUSTC_ENV, "1");
if !self.allow_static_linking {
command.env(runtime::FORCE_DYLIB_ENV, "1");
}
}
command
}
pub(crate) fn crate_dir(&self) -> &Path {
&self.tmpdir
}
pub(crate) fn src_dir(&self) -> PathBuf {
self.tmpdir.join("src")
}
pub(crate) fn deps_dir(&self) -> PathBuf {
self.target_dir().join("debug").join("deps")
}
pub(crate) fn target_dir(&self) -> PathBuf {
self.common_target_dir().join(&self.target)
}
pub(crate) fn common_target_dir(&self) -> PathBuf {
self.tmpdir.join("target")
}
}
#[derive(Debug)]
struct ErrorFormat {
format_str: &'static str,
format_trait: &'static str,
}
static ERROR_FORMATS: &[ErrorFormat] = &[
ErrorFormat {
format_str: "{}",
format_trait: "std::fmt::Display",
},
ErrorFormat {
format_str: "{:?}",
format_trait: "std::fmt::Debug",
},
ErrorFormat {
format_str: "{:#?}",
format_trait: "std::fmt::Debug",
},
];
const SEND_TEXT_PLAIN_DEF: &str = stringify!(
fn evcxr_send_text_plain(text: &str) {
use std::io::Write;
use std::io::{self};
fn try_send_text(text: &str) -> io::Result<()> {
let stdout = io::stdout();
let mut output = stdout.lock();
output.write_all(b"EVCXR_BEGIN_CONTENT text/plain\n")?;
output.write_all(text.as_bytes())?;
output.write_all(b"\nEVCXR_END_CONTENT\n")?;
Ok(())
}
if let Err(error) = try_send_text(text) {
eprintln!("Failed to send content to parent: {:?}", error);
std::process::exit(1);
}
}
);
const GET_TYPE_NAME_DEF: &str = stringify!(
pub fn evcxr_shorten_type(t: &str) -> String {
let mut r = String::with_capacity(t.len());
let mut is_skipping = false;
for c in t.chars().rev() {
if !is_skipping {
if c == ':' {
is_skipping = true;
} else {
r.push(c);
}
} else {
if !c.is_alphanumeric() && c != '_' && c != ':' {
is_skipping = false;
r.push(c);
}
}
}
r.chars().rev().collect()
}
fn evcxr_get_type_name<T>(_: &T) -> String {
evcxr_shorten_type(std::any::type_name::<T>())
}
);
const PANIC_NOTIFICATION: &str = "EVCXR_PANIC_NOTIFICATION";
pub struct EvalContextOutputs {
pub stdout: crossbeam_channel::Receiver<String>,
pub stderr: crossbeam_channel::Receiver<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct InputRequest {
pub prompt: String,
pub is_password: bool,
}
pub struct EvalCallbacks<'a> {
pub input_reader: &'a dyn Fn(InputRequest) -> String,
}
fn default_input_reader(_: InputRequest) -> String {
String::new()
}
impl<'a> Default for EvalCallbacks<'a> {
fn default() -> Self {
EvalCallbacks {
input_reader: &default_input_reader,
}
}
}
impl EvalContext {
pub fn new() -> Result<(EvalContext, EvalContextOutputs), Error> {
fix_path();
let current_exe = std::env::current_exe()?;
Self::with_subprocess_command(std::process::Command::new(current_exe))
}
fn apply_platform_specific_vars(config: &Config, command: &mut std::process::Command) {
if cfg!(not(windows)) {
return;
}
let mut path_var_value = OsString::new();
path_var_value.push(&config.deps_dir());
path_var_value.push(";");
let mut sysroot_command = std::process::Command::new("rustc");
sysroot_command.arg("--print").arg("sysroot");
path_var_value.push(format!(
"{}\\bin;",
String::from_utf8_lossy(&sysroot_command.output().unwrap().stdout).trim()
));
path_var_value.push(std::env::var("PATH").unwrap_or_default());
command.env("PATH", path_var_value);
}
#[doc(hidden)]
pub fn new_for_testing() -> (EvalContext, EvalContextOutputs) {
let testing_runtime_path = std::env::current_exe()
.unwrap()
.parent()
.unwrap()
.parent()
.unwrap()
.join("testing_runtime");
let (mut context, outputs) =
EvalContext::with_subprocess_command(std::process::Command::new(testing_runtime_path))
.unwrap();
let mut state = context.state();
state.set_offline_mode(true);
context.commit_state(state);
(context, outputs)
}
pub fn with_subprocess_command(
mut subprocess_command: std::process::Command,
) -> Result<(EvalContext, EvalContextOutputs), Error> {
let mut opt_tmpdir = None;
let mut tmpdir_path;
let init_config = InitConfig::parse_as_one_step()?;
if let Some(from_config) = init_config.tmpdir {
tmpdir_path = from_config;
} else {
let tmpdir = tempfile::tempdir()?;
tmpdir_path = PathBuf::from(tmpdir.path());
opt_tmpdir = Some(tmpdir);
}
if !tmpdir_path.is_absolute() {
tmpdir_path = std::env::current_dir()?.join(tmpdir_path);
}
let analyzer = RustAnalyzer::new(&tmpdir_path)?;
let module = Module::new()?;
let initial_config =
create_initial_config(tmpdir_path, subprocess_command.get_program().into())?;
Self::apply_platform_specific_vars(&initial_config, &mut subprocess_command);
let (stdout_sender, stdout_receiver) = crossbeam_channel::unbounded();
let (stderr_sender, stderr_receiver) = crossbeam_channel::unbounded();
let child_process = ChildProcess::new(subprocess_command, stderr_sender)?;
let initial_state = ContextState::new(initial_config.clone());
let mut context = EvalContext {
_tmpdir: opt_tmpdir,
committed_state: initial_state,
module,
child_process,
stdout_sender,
analyzer,
initial_config,
};
let outputs = EvalContextOutputs {
stdout: stdout_receiver,
stderr: stderr_receiver,
};
if context.committed_state.linker() == "lld" && context.eval("42").is_err() {
context.committed_state.set_linker("system".to_owned());
} else {
if let Err(error) = context.eval("42") {
drop(context);
let mut stderr = String::new();
while let Ok(line) = outputs.stderr.recv() {
stderr.push_str(&line);
stderr.push('\n');
}
return Err(format!("{stderr}{error}").into());
}
}
context.initial_config = context.committed_state.config.clone();
Ok((context, outputs))
}
pub fn state(&self) -> ContextState {
self.committed_state.clone()
}
pub fn eval(&mut self, code: &str) -> Result<EvalOutputs, Error> {
self.eval_with_state(code, self.state())
}
pub fn eval_with_state(
&mut self,
code: &str,
state: ContextState,
) -> Result<EvalOutputs, Error> {
let (user_code, code_info) = CodeBlock::from_original_user_code(code);
self.eval_with_callbacks(user_code, state, &code_info, &mut EvalCallbacks::default())
}
pub(crate) fn check(
&mut self,
user_code: CodeBlock,
mut state: ContextState,
code_info: &UserCodeInfo,
) -> Result<Vec<CompilationError>, Error> {
state.config.display_final_expression = false;
state.config.expand_use_statements = false;
let user_code = state.apply(user_code, &code_info.nodes)?;
let code = state.analysis_code(user_code.clone());
let errors = self.module.check(&code, &state.config)?;
Ok(state.apply_custom_errors(errors, &user_code, code_info))
}
pub(crate) fn eval_with_callbacks(
&mut self,
user_code: CodeBlock,
mut state: ContextState,
code_info: &UserCodeInfo,
callbacks: &mut EvalCallbacks,
) -> Result<EvalOutputs, Error> {
if user_code.is_empty()
&& !self
.committed_state
.state_change_can_fail_compilation(&state)
{
self.commit_state(state);
return Ok(EvalOutputs::default());
}
let mut phases = PhaseDetailsBuilder::new();
let code_out = state.apply(user_code.clone(), &code_info.nodes)?;
let mut outputs =
match self.run_statements(code_out, code_info, &mut state, &mut phases, callbacks) {
error @ Err(Error::SubprocessTerminated(_)) => {
self.restart_child_process()?;
return error;
}
Err(Error::CompilationErrors(errors)) => {
let mut errors = state.apply_custom_errors(errors, &user_code, code_info);
if errors.iter().any(|error| error.is_from_user_code()) {
errors.retain(|error| error.is_from_user_code())
}
return Err(Error::CompilationErrors(errors));
}
error @ Err(_) => return error,
Ok(x) => x,
};
self.commit_state(state);
phases.phase_complete("Execution");
outputs.phases = phases.phases;
Ok(outputs)
}
pub(crate) fn completions(
&mut self,
user_code: CodeBlock,
mut state: ContextState,
nodes: &[SyntaxNode],
offset: usize,
) -> Result<Completions> {
state.config.display_final_expression = false;
state.config.expand_use_statements = false;
let user_code = state.apply(user_code, nodes)?;
let code = state.analysis_code(user_code);
let wrapped_offset = code.user_offset_to_output_offset(offset)?;
if state.config.debug_mode {
let mut s = code.code_string();
s.insert_str(wrapped_offset, "<|>");
println!("=========\n{s}\n==========");
}
self.analyzer.set_source(code.code_string())?;
let mut completions = self.analyzer.completions(wrapped_offset)?;
completions.start_offset = code.output_offset_to_user_offset(completions.start_offset)?;
completions.end_offset = code.output_offset_to_user_offset(completions.end_offset)?;
completions.completions.retain(|c| {
c.code != "evcxr_variable_store"
&& c.code != "evcxr_internal_runtime"
&& c.code != "evcxr_analysis_wrapper"
});
Ok(completions)
}
pub fn hover(&mut self, code: &str, state: &mut ContextState) -> Result<(String, String)> {
let (modified_code, hover_offset) = if code == "let" {
(String::from("let _ = 1;"), 0)
} else if code.ends_with('(') {
(format!("{});", code), code.len() - 1)
} else {
(format!("{};", code), code.len())
};
let (user_code, code_info) = CodeBlock::from_original_user_code(&modified_code);
let user_code = state.apply(user_code, &code_info.nodes)?;
let pad_code = state.analysis_code(user_code);
self.analyzer.set_source(pad_code.code_string())?;
let wrapped_offset = pad_code.user_offset_to_output_offset(hover_offset)? as u32;
let text_range = TextRange::new(wrapped_offset.into(), wrapped_offset.into());
let hover_text = self.analyzer.hover(text_range, false)?;
let hover_markdown = self.analyzer.hover(text_range, true)?;
match (hover_text, hover_markdown) {
(Some(data_text), Some(data_markdown)) => Ok((
data_text.info.markup.into(),
data_markdown.info.markup.into(),
)),
_ => Ok((
"No documentation found".into(),
"No documentation found".into(),
)),
}
}
pub fn last_source(&self) -> Result<String, std::io::Error> {
std::fs::read_to_string(self.state().config.src_dir().join("lib.rs"))
}
pub fn set_opt_level(&mut self, level: &str) -> Result<(), Error> {
self.committed_state.set_opt_level(level)
}
pub fn set_time_passes(&mut self, value: bool) {
self.committed_state.set_time_passes(value);
}
pub fn set_preserve_vars_on_panic(&mut self, value: bool) {
self.committed_state.set_preserve_vars_on_panic(value);
}
pub fn set_error_format(&mut self, value: &str) -> Result<(), Error> {
self.committed_state.set_error_format(value)
}
pub fn variables_and_types(&self) -> impl Iterator<Item = (&str, &str)> {
self.committed_state
.variable_states
.iter()
.map(|(v, t)| (v.as_str(), t.type_name.as_str()))
}
pub fn defined_item_names(&self) -> impl Iterator<Item = &str> {
self.committed_state
.items_by_name
.keys()
.map(String::as_str)
}
pub fn clear(&mut self) -> Result<(), Error> {
self.committed_state = self.cleared_state();
self.restart_child_process()
}
pub(crate) fn cleared_state(&self) -> ContextState {
ContextState::new(self.committed_state.config.clone())
}
pub fn reset_config(&mut self) {
self.committed_state.config = self.initial_config.clone();
}
pub fn process_handle(&self) -> Arc<Mutex<std::process::Child>> {
self.child_process.process_handle()
}
pub(crate) fn restart_child_process(&mut self) -> Result<(), Error> {
self.committed_state.variable_states.clear();
self.committed_state.stored_variable_states.clear();
self.child_process = self.child_process.restart()?;
Ok(())
}
pub(crate) fn last_compile_dir(&self) -> &Path {
self.committed_state.config.crate_dir()
}
fn commit_state(&mut self, mut state: ContextState) {
for variable_state in state.variable_states.values_mut() {
variable_state.definition_span = None;
}
state.stored_variable_states = state.variable_states.clone();
state.commit_old_user_code();
self.committed_state = state;
}
fn run_statements(
&mut self,
mut user_code: CodeBlock,
code_info: &UserCodeInfo,
state: &mut ContextState,
phases: &mut PhaseDetailsBuilder,
callbacks: &mut EvalCallbacks,
) -> Result<EvalOutputs, Error> {
self.write_cargo_toml(state)?;
let analysis_code = state.analysis_code(user_code.clone());
if let Err(errors) = self.fix_variable_types(state, analysis_code) {
let check_res = self.check(user_code.clone(), state.clone(), code_info)?;
if check_res.is_empty() {
return Err(errors);
}
return Err(Error::CompilationErrors(check_res));
}
let mut remaining_retries = 5;
loop {
let result = self.try_run_statements(
user_code.clone(),
state,
state.compilation_mode(),
phases,
callbacks,
);
match result {
Ok(execution_artifacts) => {
return Ok(execution_artifacts.output);
}
Err(Error::CompilationErrors(errors)) => {
if remaining_retries > 0 {
let mut fixed = HashSet::new();
for error in &errors {
self.attempt_to_fix_error(error, &mut user_code, state, &mut fixed)?;
}
if !fixed.is_empty() {
remaining_retries -= 1;
let fixed_sorted: Vec<_> = fixed.into_iter().collect();
phases.phase_complete(&fixed_sorted.join("|"));
continue;
}
}
if !user_code.is_empty() {
self.try_run_statements(
user_code,
state,
CompilationMode::NoCatchExpectError,
phases,
callbacks,
)?;
}
return Err(Error::CompilationErrors(errors));
}
Err(Error::TypeRedefinedVariablesLost(variables)) => {
for variable in &variables {
state.variable_states.remove(variable);
state.stored_variable_states.remove(variable);
self.committed_state.variable_states.remove(variable);
self.committed_state.stored_variable_states.remove(variable);
}
remaining_retries -= 1;
}
Err(error) => return Err(error),
}
}
}
fn try_run_statements(
&mut self,
user_code: CodeBlock,
state: &mut ContextState,
compilation_mode: CompilationMode,
phases: &mut PhaseDetailsBuilder,
callbacks: &mut EvalCallbacks,
) -> Result<ExecutionArtifacts, Error> {
let code = state.code_to_compile(user_code, compilation_mode);
let so_file = self.module.compile(&code, &state.config)?;
if compilation_mode == CompilationMode::NoCatchExpectError {
return Ok(ExecutionArtifacts {
output: EvalOutputs::new(),
});
}
phases.phase_complete("Final compile");
let output = self.run_and_capture_output(state, &so_file, callbacks)?;
Ok(ExecutionArtifacts { output })
}
pub(crate) fn write_cargo_toml(&self, state: &ContextState) -> Result<()> {
self.module.write_cargo_toml(state)?;
self.module.write_config_toml(state)?;
Ok(())
}
fn fix_variable_types(
&mut self,
state: &mut ContextState,
code: CodeBlock,
) -> Result<(), Error> {
self.analyzer.set_source(code.code_string())?;
for (
variable_name,
VariableInfo {
type_name,
is_mutable,
},
) in self.analyzer.top_level_variables("evcxr_analysis_wrapper")
{
if variable_name == "evcxr_variable_store" {
continue;
}
let type_name = match type_name {
TypeName::Named(x) => x,
TypeName::Closure => bail!(
"The variable `{}` is a closure, which cannot be persisted.\n\
You can however persist closures if you box them. e.g.:\n\
let f: Box<dyn Fn()> = Box::new(|| {{println!(\"foo\")}});\n\
Alternatively, you can prevent evcxr from attempting to persist\n\
the variable by wrapping your code in braces.",
variable_name
),
TypeName::Unknown => bail!(
"Couldn't automatically determine type of variable `{}`.\n\
Please give it an explicit type.",
variable_name
),
};
let type_name = replace_reserved_words_in_type(&type_name);
state
.variable_states
.entry(variable_name)
.or_insert_with(|| VariableState {
type_name: String::new(),
is_mut: is_mutable,
move_state: VariableMoveState::New,
definition_span: None,
})
.type_name = type_name;
}
Ok(())
}
fn run_and_capture_output(
&mut self,
state: &mut ContextState,
so_file: &SoFile,
callbacks: &mut EvalCallbacks,
) -> Result<EvalOutputs, Error> {
let mut output = EvalOutputs::new();
let fn_name = state.current_user_fn_name();
self.child_process.send(&format!(
"LOAD_AND_RUN {} {}",
so_file.path.to_string_lossy(),
fn_name,
))?;
state.build_num += 1;
let mut got_panic = false;
let mut lost_variables = Vec::new();
static MIME_OUTPUT: Lazy<Regex> =
Lazy::new(|| Regex::new("EVCXR_BEGIN_CONTENT ([^ ]+)").unwrap());
loop {
let line = self.child_process.recv_line()?;
if line == runtime::EVCXR_EXECUTION_COMPLETE {
break;
}
if line == PANIC_NOTIFICATION {
got_panic = true;
} else if line.starts_with(evcxr_input::GET_CMD) {
let is_password = line.starts_with(evcxr_input::GET_CMD_PASSWORD);
let prompt = line.split(':').nth(1).unwrap_or_default().to_owned();
self.child_process
.send(&(callbacks.input_reader)(InputRequest {
prompt,
is_password,
}))?;
} else if line == evcxr_internal_runtime::USER_ERROR_OCCURRED {
state
.variable_states
.retain(|_variable_name, variable_state| {
variable_state.move_state != VariableMoveState::New
});
} else if let Some(variable_name) =
line.strip_prefix(evcxr_internal_runtime::VARIABLE_CHANGED_TYPE)
{
lost_variables.push(variable_name.to_owned());
} else if let Some(captures) = MIME_OUTPUT.captures(&line) {
let mime_type = captures[1].to_owned();
let mut content = String::new();
loop {
let line = self.child_process.recv_line()?;
if line == "EVCXR_END_CONTENT" {
break;
}
if line == PANIC_NOTIFICATION {
got_panic = true;
break;
}
if !content.is_empty() {
content.push('\n');
}
content.push_str(&line);
}
output.content_by_mime_type.insert(mime_type, content);
} else {
let _ = self.stdout_sender.send(line);
}
}
if got_panic {
state
.variable_states
.retain(|_variable_name, variable_state| {
variable_state.move_state != VariableMoveState::New
});
} else if !lost_variables.is_empty() {
return Err(Error::TypeRedefinedVariablesLost(lost_variables));
}
Ok(output)
}
fn attempt_to_fix_error(
&mut self,
error: &CompilationError,
user_code: &mut CodeBlock,
state: &mut ContextState,
fixed_errors: &mut HashSet<&'static str>,
) -> Result<(), Error> {
for code_origin in &error.code_origins {
match code_origin {
CodeKind::PackVariable { variable_name } => {
if error.code() == Some("E0382") {
state.variable_states.remove(variable_name);
fixed_errors.insert("Captured value");
} else if error.code() == Some("E0425") {
state.variable_states.remove(variable_name);
fixed_errors.insert("Variable moved");
} else if error.code() == Some("E0603") {
if let Some(variable_state) = state.variable_states.remove(variable_name) {
bail!(
"Failed to determine type of variable `{}`. rustc suggested type \
{}, but that's private. Sometimes adding an extern crate will help \
rustc suggest the correct public type name, or you can give an \
explicit type.",
variable_name,
variable_state.type_name
);
}
} else if error.code() == Some("E0562")
|| (error.code().is_none() && error.code_origins.len() == 1)
{
return non_persistable_type_error(
variable_name,
&state.variable_states[variable_name].type_name,
);
}
}
CodeKind::WithFallback(fallback) => {
user_code.apply_fallback(fallback);
fixed_errors.insert("Fallback");
}
CodeKind::OriginalUserCode(_) | CodeKind::OtherUserCode => {
if error.code() == Some("E0728") && !state.async_mode {
state.async_mode = true;
if !state.external_deps.contains_key("tokio") {
state.add_dep(
"tokio",
"{version=\"1.34.0\", features=[\"rt\", \"rt-multi-thread\"]}",
)?;
self.write_cargo_toml(state)?;
}
fixed_errors.insert("Enabled async mode");
} else if error.code() == Some("E0277") && !state.allow_question_mark {
state.allow_question_mark = true;
fixed_errors.insert("Allow question mark");
} else if error.code() == Some("E0658")
&& error
.message()
.contains("`let` expressions in this position are experimental")
{
bail!("Looks like you're missing a semicolon");
}
}
_ => {}
}
}
Ok(())
}
}
fn non_persistable_type_error(variable_name: &str, actual_type: &str) -> Result<(), Error> {
bail!(
"The variable `{}` has type `{}` which cannot be persisted.\n\
You might be able to fix this by creating a `Box<dyn YourType>`. e.g.\n\
let v: Box<dyn core::fmt::Debug> = Box::new(foo());\n\
Alternatively, you can prevent evcxr from attempting to persist\n\
the variable by wrapping your code in braces.",
variable_name,
actual_type
);
}
fn fix_path() {
if which::which("cargo").is_err() {
if let Ok(current_exe) = std::env::current_exe() {
if let Some(bin_dir) = current_exe.parent() {
if bin_dir.join("cargo").exists() {
if let Some(mut path) = std::env::var_os("PATH") {
if cfg!(windows) {
path.push(";");
} else {
path.push(":");
}
path.push(bin_dir);
std::env::set_var("PATH", path);
}
}
}
}
}
}
fn type_is_fully_specified(ty: &ast::Type) -> bool {
!AstNode::syntax(ty)
.descendants()
.any(|n| n.kind() == SyntaxKind::INFER_TYPE)
}
#[derive(Debug)]
pub struct PhaseDetails {
pub name: String,
pub duration: Duration,
}
struct PhaseDetailsBuilder {
start: Instant,
phases: Vec<PhaseDetails>,
}
impl PhaseDetailsBuilder {
fn new() -> PhaseDetailsBuilder {
PhaseDetailsBuilder {
start: Instant::now(),
phases: Vec::new(),
}
}
fn phase_complete(&mut self, name: &str) {
let new_start = Instant::now();
self.phases.push(PhaseDetails {
name: name.to_owned(),
duration: new_start.duration_since(self.start),
});
self.start = new_start;
}
}
#[derive(Default, Debug)]
pub struct EvalOutputs {
pub content_by_mime_type: HashMap<String, String>,
pub timing: Option<Duration>,
pub phases: Vec<PhaseDetails>,
}
impl EvalOutputs {
pub fn new() -> EvalOutputs {
EvalOutputs {
content_by_mime_type: HashMap::new(),
timing: None,
phases: Vec::new(),
}
}
pub fn text_html(text: String, html: String) -> EvalOutputs {
let mut out = EvalOutputs::new();
out.content_by_mime_type
.insert("text/plain".to_owned(), text);
out.content_by_mime_type
.insert("text/html".to_owned(), html);
out
}
pub fn is_empty(&self) -> bool {
self.content_by_mime_type.is_empty()
}
pub fn get(&self, mime_type: &str) -> Option<&str> {
self.content_by_mime_type.get(mime_type).map(String::as_str)
}
pub fn merge(&mut self, mut other: EvalOutputs) {
for (mime_type, content) in other.content_by_mime_type {
self.content_by_mime_type
.entry(mime_type)
.or_default()
.push_str(&content);
}
self.timing = match (self.timing.take(), other.timing) {
(Some(t1), Some(t2)) => Some(t1 + t2),
(t1, t2) => t1.or(t2),
};
self.phases.append(&mut other.phases);
}
}
#[derive(Clone, Debug)]
struct VariableState {
type_name: String,
is_mut: bool,
move_state: VariableMoveState,
definition_span: Option<UserCodeSpan>,
}
#[derive(Clone, Debug)]
struct UserCodeSpan {
segment_index: usize,
range: TextRange,
}
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
enum VariableMoveState {
New,
Available,
}
struct ExecutionArtifacts {
output: EvalOutputs,
}
#[derive(Eq, PartialEq, Copy, Clone)]
enum CompilationMode {
RunAndCatchPanics,
NoCatch,
NoCatchExpectError,
}
#[derive(Clone, Debug)]
pub struct ContextState {
items_by_name: HashMap<String, CodeBlock>,
unnamed_items: Vec<CodeBlock>,
pub(crate) external_deps: HashMap<String, ExternalCrate>,
extern_crate_stmts: HashMap<String, String>,
variable_states: HashMap<String, VariableState>,
stored_variable_states: HashMap<String, VariableState>,
attributes: HashMap<String, CodeBlock>,
async_mode: bool,
allow_question_mark: bool,
build_num: i32,
pub(crate) config: Config,
}
impl ContextState {
fn new(config: Config) -> ContextState {
ContextState {
items_by_name: HashMap::new(),
unnamed_items: vec![],
external_deps: HashMap::new(),
extern_crate_stmts: HashMap::new(),
variable_states: HashMap::new(),
stored_variable_states: HashMap::new(),
attributes: HashMap::new(),
async_mode: false,
allow_question_mark: false,
build_num: 0,
config,
}
}
pub fn time_passes(&self) -> bool {
self.config.time_passes
}
pub fn set_time_passes(&mut self, value: bool) {
self.config.time_passes = value;
}
pub fn set_offline_mode(&mut self, value: bool) {
self.config.offline_mode = value;
}
pub fn set_allow_static_linking(&mut self, value: bool) {
self.config.allow_static_linking = value;
}
pub fn set_sccache(&mut self, enabled: bool) -> Result<(), Error> {
self.config.set_sccache(enabled)
}
pub fn set_cache_bytes(&mut self, bytes: u64) {
self.config.set_cache_bytes(bytes)
}
pub fn cache_bytes(&mut self) -> u64 {
self.config.cache_bytes()
}
pub fn sccache(&self) -> bool {
self.config.sccache()
}
pub fn set_error_format(&mut self, format_str: &str) -> Result<(), Error> {
for format in ERROR_FORMATS {
if format.format_str == format_str {
self.config.error_fmt = format;
return Ok(());
}
}
bail!(
"Unsupported error format string. Available options: {}",
ERROR_FORMATS
.iter()
.map(|f| f.format_str)
.collect::<Vec<_>>()
.join(", ")
);
}
pub fn error_format(&self) -> &str {
self.config.error_fmt.format_str
}
pub fn error_format_trait(&self) -> &str {
self.config.error_fmt.format_trait
}
pub fn set_linker(&mut self, linker: String) {
self.config.linker = linker;
}
pub fn linker(&self) -> &str {
&self.config.linker
}
pub fn set_codegen_backend(&mut self, value: String) {
self.config.codegen_backend = if value == "default" {
None
} else {
Some(value)
};
}
pub fn codegen_backend(&mut self) -> &str {
self.config.codegen_backend.as_deref().unwrap_or("default")
}
pub fn preserve_vars_on_panic(&self) -> bool {
self.config.preserve_vars_on_panic
}
pub fn offline_mode(&self) -> bool {
self.config.offline_mode
}
pub fn set_preserve_vars_on_panic(&mut self, value: bool) {
self.config.preserve_vars_on_panic = value;
}
pub fn debug_mode(&self) -> bool {
self.config.debug_mode
}
pub fn set_debug_mode(&mut self, debug_mode: bool) {
self.config.debug_mode = debug_mode;
}
pub fn opt_level(&self) -> &str {
&self.config.opt_level
}
pub fn set_opt_level(&mut self, level: &str) -> Result<(), Error> {
if level.is_empty() {
bail!("Optimization level cannot be an empty string");
}
self.config.opt_level = level.to_owned();
Ok(())
}
pub fn output_format(&self) -> &str {
&self.config.output_format
}
pub fn set_output_format(&mut self, output_format: String) {
self.config.output_format = output_format;
}
pub fn display_types(&self) -> bool {
self.config.display_types
}
pub fn set_display_types(&mut self, display_types: bool) {
self.config.display_types = display_types;
}
pub fn set_toolchain(&mut self, value: &str) -> Result<()> {
if let Some(rustc_path) = rustup_tool_path(Some(value), "rustc") {
self.config.core_extern = core_extern(&rustc_path)?;
self.config.rustc_path = rustc_path;
}
if let Some(cargo_path) = rustup_tool_path(Some(value), "cargo") {
self.config.cargo_path = cargo_path;
}
self.config.toolchain = value.to_owned();
Ok(())
}
pub fn set_build_env(&mut self, key: &str, value: &str) {
self.config
.build_envs
.insert(key.to_owned(), value.to_owned());
}
pub fn toolchain(&mut self) -> &str {
&self.config.toolchain
}
pub fn add_dep(&mut self, dep: &str, dep_config: &str) -> Result<(), Error> {
if let Some(existing) = self.external_deps.get(dep) {
if existing.config == dep_config {
return Ok(());
}
}
let external = ExternalCrate::new(dep.to_owned(), dep_config.to_owned())?;
crate::cargo_metadata::validate_dep(&external.name, &external.config, &self.config)?;
self.external_deps.insert(dep.to_owned(), external);
Ok(())
}
pub fn add_local_dep(&mut self, dep: &str) -> Result<(), Error> {
let name = cargo_metadata::parse_crate_name(dep)?;
self.add_dep(&name, &format!("{{ path = \"{}\" }}", dep))
}
pub(crate) fn clear_non_debug_relevant_fields(&mut self) {
self.config.tmpdir = PathBuf::from("redacted");
if self.config.sccache.is_some() {
self.config.sccache = Some(PathBuf::from("redacted"));
}
}
fn apply_custom_errors(
&self,
errors: Vec<CompilationError>,
user_code: &CodeBlock,
code_info: &UserCodeInfo,
) -> Vec<CompilationError> {
errors
.into_iter()
.filter_map(|error| self.customize_error(error, user_code))
.map(|mut error| {
error.fill_lines(code_info);
error
})
.collect()
}
fn customize_error(
&self,
error: CompilationError,
user_code: &CodeBlock,
) -> Option<CompilationError> {
for origin in &error.code_origins {
if let CodeKind::PackVariable { variable_name } = origin {
if let Some(definition_span) = &self.variable_states[variable_name].definition_span
{
if let Some(segment) =
user_code.segment_with_index(definition_span.segment_index)
{
if let Some(span) = Span::from_segment(segment, definition_span.range) {
return self.replacement_for_pack_variable_error(
variable_name,
span,
segment,
&error,
);
}
}
}
}
}
Some(error)
}
fn replacement_for_pack_variable_error(
&self,
variable_name: &str,
variable_span: Span,
segment: &Segment,
error: &CompilationError,
) -> Option<CompilationError> {
let message = match error.code().unwrap_or("") {
"E0382" | "E0505" => {
return None;
}
"E0597" => {
format!(
"The variable `{variable_name}` contains a reference with a non-static lifetime so\n\
can't be persisted. You can prevent this error by making sure that the\n\
variable goes out of scope - i.e. wrapping the code in {{}}."
)
}
_ => {
return Some(error.clone());
}
};
Some(CompilationError::from_segment_span(
segment,
SpannedMessage::from_segment_span(segment, variable_span),
message,
))
}
fn state_change_can_fail_compilation(&self, new_state: &ContextState) -> bool {
(self.extern_crate_stmts != new_state.extern_crate_stmts
&& !new_state.extern_crate_stmts.is_empty())
|| (self.external_deps != new_state.external_deps
&& !new_state.external_deps.is_empty())
|| (self.items_by_name != new_state.items_by_name
&& !new_state.items_by_name.is_empty())
|| (self.config.sccache != new_state.config.sccache)
}
pub(crate) fn format_cargo_deps(&self) -> String {
self.external_deps
.values()
.map(|krate| format!("{} = {}\n", krate.name, krate.config))
.collect::<Vec<_>>()
.join("")
}
fn compilation_mode(&self) -> CompilationMode {
if self.config.preserve_vars_on_panic {
CompilationMode::RunAndCatchPanics
} else {
CompilationMode::NoCatch
}
}
fn analysis_code(&self, user_code: CodeBlock) -> CodeBlock {
let mut code = CodeBlock::new()
.generated("#![allow(unused_imports, unused_mut, dead_code)]")
.add_all(self.attributes_code())
.add_all(self.items_code())
.add_all(self.error_trait_code(true))
.generated("fn evcxr_variable_store<T: 'static>(_: T) {}")
.generated("#[allow(unused_variables)]")
.generated("async fn evcxr_analysis_wrapper(");
for (var_name, state) in &self.stored_variable_states {
code = code.generated(format!(
"{}{}: {},",
if state.is_mut { "mut " } else { "" },
var_name,
state.type_name
));
}
code = code
.generated(") -> Result<(), EvcxrUserCodeError> {")
.add_all(user_code);
for var_name in self.variable_states.keys() {
code.pack_variable(
var_name.clone(),
format!("evcxr_variable_store({var_name});"),
);
}
code = code.generated("Ok(())").generated("}");
code
}
fn code_to_compile(
&self,
user_code: CodeBlock,
compilation_mode: CompilationMode,
) -> CodeBlock {
let mut code = CodeBlock::new()
.generated("#![allow(unused_imports, unused_mut, dead_code)]")
.add_all(self.attributes_code())
.add_all(self.items_code());
let has_user_code = !user_code.is_empty();
if has_user_code {
code = code.add_all(self.wrap_user_code(user_code, compilation_mode));
} else {
code = code
.generated("#[no_mangle]")
.generated(format!(
"pub extern \"C\" fn {}(",
self.current_user_fn_name()
))
.generated("mut x: *mut std::os::raw::c_void) -> *mut std::os::raw::c_void {x}");
}
code
}
fn items_code(&self) -> CodeBlock {
let mut code = CodeBlock::new().add_all(self.get_imports());
for item in self.items_by_name.values().chain(self.unnamed_items.iter()) {
code = code.add_all(item.clone());
}
code
}
fn attributes_code(&self) -> CodeBlock {
let mut code = CodeBlock::new();
for attrib in self.attributes.values() {
code = code.add_all(attrib.clone());
}
code
}
fn error_trait_code(&self, for_analysis: bool) -> CodeBlock {
CodeBlock::new().generated(format!(
r#"
struct EvcxrUserCodeError {{}}
impl<T: {}> From<T> for EvcxrUserCodeError {{
fn from(error: T) -> Self {{
eprintln!("{}", error);
{}
EvcxrUserCodeError {{}}
}}
}}
"#,
self.config.error_fmt.format_trait,
self.config.error_fmt.format_str,
if for_analysis {
""
} else {
"println!(\"{}\", evcxr_internal_runtime::USER_ERROR_OCCURRED);"
}
))
}
fn wrap_user_code(
&self,
mut user_code: CodeBlock,
compilation_mode: CompilationMode,
) -> CodeBlock {
let needs_variable_store = !self.variable_states.is_empty()
|| !self.stored_variable_states.is_empty()
|| self.async_mode
|| self.allow_question_mark;
let mut code = CodeBlock::new();
if self.allow_question_mark {
code = code.add_all(self.error_trait_code(false));
}
if needs_variable_store {
code = code
.generated("mod evcxr_internal_runtime {")
.generated(include_str!("evcxr_internal_runtime.rs"))
.generated("}");
}
code = code.generated("#[no_mangle]").generated(format!(
"pub extern \"C\" fn {}(",
self.current_user_fn_name()
));
if needs_variable_store {
code = code
.generated("mut evcxr_variable_store: *mut evcxr_internal_runtime::VariableStore)")
.generated(" -> *mut evcxr_internal_runtime::VariableStore {")
.generated("if evcxr_variable_store.is_null() {")
.generated(
" evcxr_variable_store = evcxr_internal_runtime::create_variable_store();",
)
.generated("}")
.generated("let evcxr_variable_store = unsafe {&mut *evcxr_variable_store};")
.add_all(self.check_variable_statements())
.add_all(self.load_variable_statements());
user_code = user_code.add_all(self.store_variable_statements(VariableMoveState::New));
} else {
code = code.generated("evcxr_variable_store: *mut u8) -> *mut u8 {");
}
if self.async_mode {
user_code = CodeBlock::new()
.generated(stringify!(
let mut mutex = evcxr_variable_store.lazy_arc("evcxr_tokio_runtime",
|| std::sync::Mutex::new(tokio::runtime::Runtime::new().unwrap())
);
match mutex.lock() {
Ok(guard) => guard,
Err(poisoned) => poisoned.into_inner(),
}
))
.generated(".block_on(async {")
.add_all(user_code);
if self.allow_question_mark {
user_code = CodeBlock::new()
.add_all(user_code)
.generated("Ok::<(), EvcxrUserCodeError>(())");
}
user_code = user_code.generated("});")
} else if self.allow_question_mark {
user_code = CodeBlock::new()
.generated("let _ = (|| -> std::result::Result<(), EvcxrUserCodeError> {")
.add_all(user_code)
.generated("Ok(())})();");
}
if compilation_mode == CompilationMode::RunAndCatchPanics {
if needs_variable_store {
code = code
.generated("match std::panic::catch_unwind(")
.generated(" std::panic::AssertUnwindSafe(||{")
.add_all(user_code)
.generated("})) { ")
.generated(" Ok(_) => {}")
.generated(" Err(_) => {")
.generated(format!(" println!(\"{PANIC_NOTIFICATION}\");"))
.generated("}}");
} else {
code = code
.generated("if std::panic::catch_unwind(||{")
.add_all(user_code)
.generated("}).is_err() {")
.generated(format!(" println!(\"{PANIC_NOTIFICATION}\");"))
.generated("}");
}
} else {
code = code.add_all(user_code);
}
if needs_variable_store {
code = code.add_all(self.store_variable_statements(VariableMoveState::Available));
}
code = code.generated("evcxr_variable_store");
code.generated("}")
}
fn store_variable_statements(&self, move_state: VariableMoveState) -> CodeBlock {
let mut statements = CodeBlock::new();
for (var_name, var_state) in &self.variable_states {
if var_state.move_state == move_state {
statements.pack_variable(
var_name.clone(),
format!(
"evcxr_variable_store.put_variable::<{}>(stringify!({var_name}), {var_name});",
var_state.type_name
),
);
}
}
statements
}
fn check_variable_statements(&self) -> CodeBlock {
let mut statements = CodeBlock::new().generated("{let mut vars_ok = true;");
for (var_name, var_state) in &self.stored_variable_states {
statements = statements.generated(format!(
"vars_ok &= evcxr_variable_store.check_variable::<{}>(stringify!({var_name}));",
var_state.type_name
));
}
statements.generated("if !vars_ok {return evcxr_variable_store;}}")
}
fn load_variable_statements(&self) -> CodeBlock {
let mut statements = CodeBlock::new();
for (var_name, var_state) in &self.stored_variable_states {
let mutability = if var_state.is_mut { "mut " } else { "" };
statements.load_variable(format!(
"let {}{} = evcxr_variable_store.take_variable::<{}>(stringify!({}));",
mutability, var_name, var_state.type_name, var_name
));
}
statements
}
fn current_user_fn_name(&self) -> String {
format!("run_user_code_{}", self.build_num)
}
fn get_imports(&self) -> CodeBlock {
let mut extern_stmts = CodeBlock::new();
for stmt in self.extern_crate_stmts.values() {
extern_stmts = extern_stmts.other_user_code(stmt.clone());
}
extern_stmts
}
fn commit_old_user_code(&mut self) {
for block in self.items_by_name.values_mut() {
block.commit_old_user_code();
}
for block in self.unnamed_items.iter_mut() {
block.commit_old_user_code();
}
for block in self.attributes.values_mut() {
block.commit_old_user_code();
}
}
fn apply(&mut self, user_code: CodeBlock, nodes: &[SyntaxNode]) -> Result<CodeBlock, Error> {
for variable_state in self.variable_states.values_mut() {
variable_state.move_state = VariableMoveState::Available;
}
let mut code_out = CodeBlock::new();
let mut previous_item_name = None;
let num_statements = user_code.segments.len();
for (statement_index, segment) in user_code.segments.into_iter().enumerate() {
let node = if let CodeKind::OriginalUserCode(meta) = &segment.kind {
&nodes[meta.node_index]
} else {
code_out = code_out.with_segment(segment);
continue;
};
if let Some(let_stmt) = ast::LetStmt::cast(node.clone()) {
if let Some(pat) = let_stmt.pat() {
self.record_new_locals(pat, let_stmt.ty(), &segment, node.text_range());
code_out = code_out.with_segment(segment);
}
} else if ast::Attr::can_cast(node.kind()) {
self.attributes.insert(
node.text().to_string(),
CodeBlock::new().with_segment(segment),
);
} else if ast::Expr::can_cast(node.kind()) {
if statement_index == num_statements - 1 {
if self.config.display_final_expression {
code_out = code_out.code_with_fallback(
CodeBlock::new()
.generated("(")
.with_segment(segment.clone())
.generated(").evcxr_display();")
.code_string(),
if self.config.display_types {
CodeBlock::new()
.generated(SEND_TEXT_PLAIN_DEF)
.generated(GET_TYPE_NAME_DEF)
.generated("{ let r = &(")
.with_segment(segment)
.generated(format!(
"); evcxr_send_text_plain(&format!(\": {{}} = {}\", evcxr_get_type_name(r), r)); }};",
self.config.output_format
))
} else {
CodeBlock::new()
.generated(SEND_TEXT_PLAIN_DEF)
.generated(format!(
"evcxr_send_text_plain(&format!(\"{}\",&(\n",
self.config.output_format
))
.with_segment(segment)
.generated(")));")
},
);
} else {
code_out = code_out
.generated("let _ = ")
.with_segment(segment)
.generated(";");
}
} else {
code_out = code_out.with_segment(segment);
}
} else if let Some(item) = ast::Item::cast(node.clone()) {
match item {
ast::Item::ExternCrate(extern_crate) => {
if let Some(crate_name) = extern_crate.name_ref() {
let crate_name = crate_name.text().to_string();
if !self.dependency_lib_names()?.contains(&crate_name) {
self.external_deps
.entry(crate_name.clone())
.or_insert_with(|| {
ExternalCrate::new(crate_name.clone(), "\"*\"".to_owned())
.unwrap()
});
}
self.extern_crate_stmts
.insert(crate_name, segment.code.clone());
}
}
ast::Item::MacroRules(macro_rules) => {
if let Some(name) = ast::HasName::name(¯o_rules) {
let item_block = CodeBlock::new().with_segment(segment);
self.items_by_name
.insert(name.text().to_string(), item_block);
} else {
code_out = code_out.with_segment(segment);
}
}
ast::Item::Use(use_stmt) => {
if let Some(use_tree) = use_stmt.use_tree() {
if self.config.expand_use_statements {
crate::use_trees::use_tree_names_do(&use_tree, &mut |import| {
match import {
Import::Unnamed(code) => {
self.unnamed_items
.push(CodeBlock::new().other_user_code(code));
}
Import::Named { name, code } => {
self.items_by_name.insert(
name,
CodeBlock::new().other_user_code(code),
);
}
}
});
} else {
crate::use_trees::use_tree_names_do(&use_tree, &mut |import| {
if let Import::Named { name, .. } = import {
self.items_by_name.remove(&name);
}
});
self.unnamed_items
.push(CodeBlock::new().with_segment(segment));
}
} else {
code_out = code_out.with_segment(segment);
}
}
item => {
let item_block = CodeBlock::new().with_segment(segment);
if let Some(item_name) = item::item_name(&item) {
*self.items_by_name.entry(item_name.to_owned()).or_default() =
item_block;
previous_item_name = Some(item_name);
} else if let Some(item_name) = &previous_item_name {
self.items_by_name
.get_mut(item_name)
.unwrap()
.modify(move |block_for_name| block_for_name.add_all(item_block));
} else {
self.unnamed_items.push(item_block);
}
}
}
} else {
code_out = code_out.with_segment(segment);
}
}
Ok(code_out)
}
fn dependency_lib_names(&self) -> Result<Vec<String>> {
cargo_metadata::get_library_names(&self.config)
}
fn record_new_locals(
&mut self,
pat: ast::Pat,
opt_ty: Option<ast::Type>,
segment: &Segment,
let_stmt_range: TextRange,
) {
match pat {
ast::Pat::IdentPat(ident) => self.record_local(ident, opt_ty, segment, let_stmt_range),
ast::Pat::RecordPat(ref pat_struct) => {
if let Some(record_fields) = pat_struct.record_pat_field_list() {
for field in record_fields.fields() {
if let Some(pat) = field.pat() {
self.record_new_locals(pat, None, segment, let_stmt_range);
}
}
}
}
ast::Pat::TuplePat(ref pat_tuple) => {
for pat in pat_tuple.fields() {
self.record_new_locals(pat, None, segment, let_stmt_range);
}
}
ast::Pat::TupleStructPat(ref pat_tuple) => {
for pat in pat_tuple.fields() {
self.record_new_locals(pat, None, segment, let_stmt_range);
}
}
_ => {}
}
}
fn record_local(
&mut self,
pat_ident: ast::IdentPat,
opt_ty: Option<ast::Type>,
segment: &Segment,
let_stmt_range: TextRange,
) {
let type_name = match opt_ty {
Some(ty) if type_is_fully_specified(&ty) => format!("{}", AstNode::syntax(&ty).text()),
_ => "String".to_owned(),
};
if let Some(name) = ast::HasName::name(&pat_ident) {
self.variable_states.insert(
name.text().to_string(),
VariableState {
type_name,
is_mut: pat_ident.mut_token().is_some(),
move_state: VariableMoveState::New,
definition_span: segment.sequence.map(|segment_index| {
let range = name.syntax().text_range() - let_stmt_range.start();
UserCodeSpan {
segment_index,
range,
}
}),
},
);
}
}
}
fn rustup_tool_path(toolchain: Option<&str>, tool: &str) -> Option<PathBuf> {
let mut cmd = Command::new("rustup");
if let Some(toolchain) = toolchain {
cmd.arg("+".to_owned() + toolchain);
}
let output = cmd.arg("which").arg(tool).output().ok()?;
if !output.status.success() {
return None;
}
Some(PathBuf::from(
std::str::from_utf8(&output.stdout).ok()?.trim().to_owned(),
))
}
fn default_tool_path(tool: &str, fallback: &str) -> Result<PathBuf> {
if let Some(path) = rustup_tool_path(None, tool) {
return Ok(path);
}
if let Ok(path) = which::which(tool) {
return Ok(path);
}
let path = PathBuf::from(fallback);
if path.exists() {
if let Some(home) = dirs::home_dir() {
if path.starts_with(home) {
return Ok(path);
}
}
}
anyhow::bail!("Cannot find `{}` binary", tool);
}
fn default_cargo_path() -> Result<PathBuf> {
const BUILD_TIME_CARGO_PATH: &str = include_str!(concat!(env!("OUT_DIR"), "/cargo_path"));
default_tool_path("cargo", BUILD_TIME_CARGO_PATH)
}
fn default_rustc_path() -> Result<PathBuf> {
const BUILD_TIME_RUSTC_PATH: &str = include_str!(concat!(env!("OUT_DIR"), "/rustc_path"));
default_tool_path("rustc", BUILD_TIME_RUSTC_PATH)
}
fn get_host_target(rustc_path: &Path) -> Result<String, Error> {
let output = match Command::new(rustc_path).arg("-Vv").output() {
Ok(o) => o,
Err(error) => bail!("Failed to run rustc: {}", error),
};
let stdout = std::str::from_utf8(&output.stdout)?;
let stderr = std::str::from_utf8(&output.stderr)?;
for line in stdout.lines() {
if let Some(host) = line.strip_prefix("host: ") {
return Ok(host.to_owned());
}
}
bail!(
"`{} -Vv` didn't output a host line.\n{}\n{}",
rustc_path.display(),
stdout,
stderr
);
}
fn replace_reserved_words_in_type(ty: &str) -> String {
static RESERVED_WORDS: Lazy<Regex> =
Lazy::new(|| Regex::new("(^|:|<)(async|try)(>|$|:)").unwrap());
RESERVED_WORDS.replace_all(ty, "${1}r#${2}${3}").to_string()
}
fn core_extern(rustc: &Path) -> Result<OsString> {
let std_lib = std_lib_path(rustc)?;
let mut result = OsString::from("core=");
result.push(std_lib.as_os_str());
Ok(result)
}
fn std_lib_path(rustc: &Path) -> Result<PathBuf> {
let libdir_bytes = std::process::Command::new(rustc)
.arg("--print")
.arg("target-libdir")
.output()?
.stdout;
let libdir = std::str::from_utf8(&libdir_bytes)?;
let dir = std::fs::read_dir(libdir.trim())?;
let prefix = format!("{}std-", crate::module::shared_object_prefix());
for entry in dir {
let Ok(entry) = entry else { continue };
if entry
.file_name()
.to_str()
.is_some_and(|file_name| file_name.starts_with(&prefix))
&& entry
.path()
.extension()
.map(|ext| ext == crate::module::shared_object_extension())
.unwrap_or(false)
{
return Ok(entry.path());
}
}
anyhow::bail!("No libstd found in {libdir}");
}
#[cfg(test)]
mod tests {
use super::*;
use ra_ap_syntax::ast::HasAttrs;
use ra_ap_syntax::SourceFile;
#[test]
fn test_replace_reserved_words_in_type() {
use super::replace_reserved_words_in_type as repl;
assert_eq!(repl("asyncstart"), "asyncstart");
assert_eq!(repl("endasync"), "endasync");
assert_eq!(repl("async::foo"), "r#async::foo");
assert_eq!(repl("foo::async::bar"), "foo::r#async::bar");
assert_eq!(repl("foo::async::async::bar"), "foo::r#async::r#async::bar");
assert_eq!(repl("Bar<async::foo::Baz>"), "Bar<r#async::foo::Baz>");
}
fn create_state() -> ContextState {
let config = Config::new(
PathBuf::from("/dummy_path"),
PathBuf::from("/dummy_evcxr_bin"),
)
.unwrap();
ContextState::new(config)
}
#[test]
fn test_attributes() {
let mut state = create_state();
let (user_code, code_info) = CodeBlock::from_original_user_code(stringify!(
#![feature(some_other_feature)]
fn foo() {}
let x = 10;
));
let user_code = state.apply(user_code, &code_info.nodes).unwrap();
let final_code = state.code_to_compile(user_code, CompilationMode::NoCatch);
let source_file = SourceFile::parse(&final_code.code_string()).ok().unwrap();
let mut attrs: Vec<String> = source_file
.attrs()
.map(|attr| attr.syntax().text().to_string().replace(' ', ""))
.collect();
attrs.sort();
assert_eq!(
attrs,
vec![
"#![allow(unused_imports,unused_mut,dead_code)]".to_owned(),
"#![feature(some_other_feature)]".to_owned(),
]
);
}
}