Remove lazy records (#12682)

# Description
Removes lazy records from the language, following from the reasons
outlined in #12622. Namely, this should make semantics more clear and
will eliminate concerns regarding maintainability.

# User-Facing Changes
- Breaking change: `lazy make` is removed.
- Breaking change: `describe --collect-lazyrecords` flag is removed.
- `sys` and `debug info` now return regular records.

# After Submitting
- Update nushell book if necessary.
- Explore new `sys` and `debug info` APIs to prevent them from taking
too long (e.g., subcommands or taking an optional column/cell-path
argument).
This commit is contained in:
Ian Manske
2024-05-03 00:36:10 +00:00
committed by GitHub
parent ad6deadf24
commit 847646e44e
32 changed files with 133 additions and 867 deletions

View File

@ -272,10 +272,6 @@ pub fn debug_string_without_formatting(value: &Value) -> String {
.collect::<Vec<_>>()
.join(" ")
),
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => debug_string_without_formatting(&val),
Err(error) => format!("{error:?}"),
},
//TODO: It would be good to drill deeper into closures.
Value::Closure { val, .. } => format!("<Closure {}>", val.block_id),
Value::Nothing { .. } => String::new(),

View File

@ -1,5 +1,4 @@
use nu_engine::command_prelude::*;
use nu_protocol::LazyRecord;
use sysinfo::{MemoryRefreshKind, Pid, ProcessRefreshKind, RefreshKind, System};
const ENV_PATH_SEPARATOR_CHAR: char = {
@ -39,14 +38,10 @@ impl Command for DebugInfo {
&self,
_engine_state: &EngineState,
_stack: &mut Stack,
_call: &Call,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let span = Span::unknown();
let record = LazySystemInfoRecord { span };
Ok(Value::lazy_record(Box::new(record), span).into_pipeline_data())
Ok(all_columns(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -58,207 +53,119 @@ impl Command for DebugInfo {
}
}
#[derive(Debug, Clone)]
struct LazySystemInfoRecord {
span: Span,
}
fn all_columns(span: Span) -> Value {
let rk = RefreshKind::new()
.with_processes(ProcessRefreshKind::everything())
.with_memory(MemoryRefreshKind::everything());
impl LazySystemInfoRecord {
fn get_column_value_with_system(
&self,
column: &str,
system_option: Option<&System>,
) -> Result<Value, ShellError> {
let pid = Pid::from(std::process::id() as usize);
match column {
"thread_id" => Ok(Value::int(get_thread_id() as i64, self.span)),
"pid" => Ok(Value::int(pid.as_u32() as i64, self.span)),
"ppid" => {
// only get information requested
let system_opt = SystemOpt::from((system_option, || {
RefreshKind::new().with_processes(ProcessRefreshKind::everything())
}));
// only get information requested
let sys = System::new_with_specifics(rk);
let system = system_opt.get_system();
// get the process information for the nushell pid
let pinfo = system.process(pid);
let pid = Pid::from(std::process::id() as usize);
let ppid = {
sys.process(pid)
.and_then(|p| p.parent())
.map(|p| Value::int(p.as_u32().into(), span))
.unwrap_or(Value::nothing(span))
};
Ok(pinfo
.and_then(|p| p.parent())
.map(|p| Value::int(p.as_u32() as i64, self.span))
.unwrap_or(Value::nothing(self.span)))
}
"system" => {
// only get information requested
let system_opt = SystemOpt::from((system_option, || {
RefreshKind::new().with_memory(MemoryRefreshKind::everything())
}));
let system = Value::record(
record! {
"total_memory" => Value::filesize(sys.total_memory() as i64, span),
"free_memory" => Value::filesize(sys.free_memory() as i64, span),
"used_memory" => Value::filesize(sys.used_memory() as i64, span),
"available_memory" => Value::filesize(sys.available_memory() as i64, span),
},
span,
);
let system = system_opt.get_system();
let process = if let Some(p) = sys.process(pid) {
let root = if let Some(path) = p.exe().and_then(|p| p.parent()) {
Value::string(path.to_string_lossy().to_string(), span)
} else {
Value::nothing(span)
};
Ok(Value::record(
record! {
"total_memory" => Value::filesize(system.total_memory() as i64, self.span),
"free_memory" => Value::filesize(system.free_memory() as i64, self.span),
"used_memory" => Value::filesize(system.used_memory() as i64, self.span),
"available_memory" => Value::filesize(system.available_memory() as i64, self.span),
},
self.span,
))
}
"process" => {
// only get information requested
let system_opt = SystemOpt::from((system_option, || {
RefreshKind::new().with_processes(ProcessRefreshKind::everything())
}));
let cwd = if let Some(path) = p.cwd() {
Value::string(path.to_string_lossy().to_string(), span)
} else {
Value::nothing(span)
};
let system = system_opt.get_system();
let pinfo = system.process(pid);
let exe_path = if let Some(path) = p.exe() {
Value::string(path.to_string_lossy().to_string(), span)
} else {
Value::nothing(span)
};
if let Some(p) = pinfo {
Ok(Value::record(
record! {
"memory" => Value::filesize(p.memory() as i64, self.span),
"virtual_memory" => Value::filesize(p.virtual_memory() as i64, self.span),
"status" => Value::string(p.status().to_string(), self.span),
"root" => {
if let Some(path) = p.exe().and_then(|p| p.parent()) {
Value::string(path.to_string_lossy().to_string(), self.span)
} else {
Value::nothing(self.span)
}
},
"cwd" => {
if let Some(path) = p.cwd() {
Value::string(path.to_string_lossy().to_string(), self.span)
}else{
Value::nothing(self.span)
}
},
"exe_path" => {
if let Some(path)= p.exe() {
Value::string(path.to_string_lossy().to_string(), self.span)
}else{
Value::nothing(self.span)
}
},
"command" => Value::string(p.cmd().join(" "), self.span),
"name" => Value::string(p.name().to_string(), self.span),
"environment" => {
let mut env_rec = Record::new();
for val in p.environ() {
if let Some((key, value)) = val.split_once('=') {
let is_env_var_a_list = {
{
#[cfg(target_family = "windows")]
{
key == "Path" || key == "PATHEXT" || key == "PSMODULEPATH" || key == "PSModulePath"
}
#[cfg(not(target_family = "windows"))]
{
key == "PATH" || key == "DYLD_FALLBACK_LIBRARY_PATH"
}
}
};
if is_env_var_a_list {
let items = value.split(ENV_PATH_SEPARATOR_CHAR).map(|r| Value::string(r.to_string(), self.span)).collect::<Vec<_>>();
env_rec.push(key.to_string(), Value::list(items, self.span));
} else if key == "LS_COLORS" { // LS_COLORS is a special case, it's a colon separated list of key=value pairs
let items = value.split(':').map(|r| Value::string(r.to_string(), self.span)).collect::<Vec<_>>();
env_rec.push(key.to_string(), Value::list(items, self.span));
} else {
env_rec.push(key.to_string(), Value::string(value.to_string(), self.span));
}
}
}
Value::record(env_rec, self.span)
},
},
self.span,
))
} else {
// If we can't get the process information, just return the system information
// only get information requested
let system_opt = SystemOpt::from((system_option, || {
RefreshKind::new().with_memory(MemoryRefreshKind::everything())
}));
let system = system_opt.get_system();
Ok(Value::record(
record! {
"total_memory" => Value::filesize(system.total_memory() as i64, self.span),
"free_memory" => Value::filesize(system.free_memory() as i64, self.span),
"used_memory" => Value::filesize(system.used_memory() as i64, self.span),
"available_memory" => Value::filesize(system.available_memory() as i64, self.span),
},
self.span,
))
let environment = {
let mut env_rec = Record::new();
for val in p.environ() {
if let Some((key, value)) = val.split_once('=') {
let is_env_var_a_list = {
{
#[cfg(target_family = "windows")]
{
key == "Path"
|| key == "PATHEXT"
|| key == "PSMODULEPATH"
|| key == "PSModulePath"
}
#[cfg(not(target_family = "windows"))]
{
key == "PATH" || key == "DYLD_FALLBACK_LIBRARY_PATH"
}
}
};
if is_env_var_a_list {
let items = value
.split(ENV_PATH_SEPARATOR_CHAR)
.map(|r| Value::string(r.to_string(), span))
.collect::<Vec<_>>();
env_rec.push(key.to_string(), Value::list(items, span));
} else if key == "LS_COLORS" {
// LS_COLORS is a special case, it's a colon separated list of key=value pairs
let items = value
.split(':')
.map(|r| Value::string(r.to_string(), span))
.collect::<Vec<_>>();
env_rec.push(key.to_string(), Value::list(items, span));
} else {
env_rec.push(key.to_string(), Value::string(value.to_string(), span));
}
}
}
_ => Err(ShellError::IncompatibleParametersSingle {
msg: format!("Unknown column: {}", column),
span: self.span,
}),
}
}
}
Value::record(env_rec, span)
};
impl<'a> LazyRecord<'a> for LazySystemInfoRecord {
fn column_names(&'a self) -> Vec<&'a str> {
vec!["thread_id", "pid", "ppid", "process", "system"]
}
Value::record(
record! {
"memory" => Value::filesize(p.memory() as i64, span),
"virtual_memory" => Value::filesize(p.virtual_memory() as i64, span),
"status" => Value::string(p.status().to_string(), span),
"root" => root,
"cwd" => cwd,
"exe_path" => exe_path,
"command" => Value::string(p.cmd().join(" "), span),
"name" => Value::string(p.name(), span),
"environment" => environment,
},
span,
)
} else {
Value::nothing(span)
};
fn get_column_value(&self, column: &str) -> Result<Value, ShellError> {
self.get_column_value_with_system(column, None)
}
fn span(&self) -> Span {
self.span
}
fn clone_value(&self, span: Span) -> Value {
Value::lazy_record(Box::new(LazySystemInfoRecord { span }), span)
}
fn collect(&'a self) -> Result<Value, ShellError> {
let rk = RefreshKind::new()
.with_processes(ProcessRefreshKind::everything())
.with_memory(MemoryRefreshKind::everything());
// only get information requested
let system = System::new_with_specifics(rk);
self.column_names()
.into_iter()
.map(|col| {
let val = self.get_column_value_with_system(col, Some(&system))?;
Ok((col.to_owned(), val))
})
.collect::<Result<Record, _>>()
.map(|record| Value::record(record, self.span()))
}
}
enum SystemOpt<'a> {
Ptr(&'a System),
Owned(Box<System>),
}
impl<'a> SystemOpt<'a> {
fn get_system(&'a self) -> &'a System {
match self {
SystemOpt::Ptr(system) => system,
SystemOpt::Owned(system) => system,
}
}
}
impl<'a, F: Fn() -> RefreshKind> From<(Option<&'a System>, F)> for SystemOpt<'a> {
fn from((system_opt, refresh_kind_create): (Option<&'a System>, F)) -> Self {
match system_opt {
Some(system) => SystemOpt::<'a>::Ptr(system),
None => SystemOpt::Owned(Box::new(System::new_with_specifics(refresh_kind_create()))),
}
}
Value::record(
record! {
"thread_id" => Value::int(get_thread_id() as i64, span),
"pid" => Value::int(pid.as_u32().into(), span),
"ppid" => ppid,
"system" => system,
"process" => process,
},
span,
)
}
fn get_thread_id() -> u64 {

View File

@ -105,18 +105,6 @@ fn getcol(
.into_pipeline_data(ctrlc)
.set_metadata(metadata))
}
Value::LazyRecord { val, .. } => {
Ok({
// Unfortunate casualty to LazyRecord's column_names not generating 'static strs
let cols: Vec<_> =
val.column_names().iter().map(|s| s.to_string()).collect();
cols.into_iter()
.map(move |x| Value::string(x, head))
.into_pipeline_data(ctrlc)
.set_metadata(metadata)
})
}
Value::Record { val, .. } => Ok(val
.into_owned()
.into_iter()

View File

@ -533,15 +533,6 @@ fn value_should_be_printed(
Value::Record { val, .. } => {
record_matches_term(val, columns_to_search, filter_config, term, span)
}
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => match val {
Value::Record { val, .. } => {
record_matches_term(&val, columns_to_search, filter_config, term, span)
}
_ => false,
},
Err(_) => false,
},
Value::Binary { .. } => false,
});
if invert {

View File

@ -44,12 +44,6 @@ impl Command for Items {
match input {
PipelineData::Empty => Ok(PipelineData::Empty),
PipelineData::Value(value, ..) => {
let value = if let Value::LazyRecord { val, .. } = value {
val.collect()?
} else {
value
};
let span = value.span();
match value {
Value::Record { val, .. } => {

View File

@ -161,20 +161,6 @@ fn values(
.cloned()
.collect::<Vec<_>>()
.into_pipeline_data_with_metadata(metadata, ctrlc)),
Value::LazyRecord { val, .. } => {
let record = match val.collect()? {
Value::Record { val, .. } => val,
_ => Err(ShellError::NushellFailedSpanned {
msg: "`LazyRecord::collect()` promises `Value::Record`".into(),
label: "Violating lazy record found here".into(),
span,
})?,
};
Ok(record
.into_owned()
.into_values()
.into_pipeline_data_with_metadata(metadata, ctrlc))
}
// Propagate errors
Value::Error { error, .. } => Err(*error),
other => Err(ShellError::OnlySupportsThisInputType {

View File

@ -135,10 +135,6 @@ pub fn value_to_json_value(v: &Value) -> Result<nu_json::Value, ShellError> {
}
nu_json::Value::Object(m)
}
Value::LazyRecord { val, .. } => {
let collected = val.collect()?;
value_to_json_value(&collected)?
}
Value::Custom { val, .. } => {
let collected = val.to_base_value(span)?;
value_to_json_value(&collected)?

View File

@ -246,9 +246,6 @@ pub(crate) fn write_value(
Value::Custom { val, .. } => {
write_value(out, &val.to_base_value(span)?, depth)?;
}
Value::LazyRecord { val, .. } => {
write_value(out, &val.collect()?, depth)?;
}
}
Ok(())
}

View File

@ -130,10 +130,6 @@ fn local_into_string(value: Value, separator: &str, config: &Config) -> String {
.map(|(x, y)| format!("{}: {}", x, local_into_string(y, ", ", config)))
.collect::<Vec<_>>()
.join(separator),
Value::LazyRecord { val, .. } => match val.collect() {
Ok(val) => local_into_string(val, separator, config),
Err(error) => format!("{error:?}"),
},
Value::Closure { val, .. } => format!("<Closure {}>", val.block_id),
Value::Nothing { .. } => String::new(),
Value::Error { error, .. } => format!("{error:?}"),

View File

@ -62,10 +62,6 @@ fn helper(engine_state: &EngineState, v: &Value) -> Result<toml::Value, ShellErr
}
toml::Value::Table(m)
}
Value::LazyRecord { val, .. } => {
let collected = val.collect()?;
helper(engine_state, &collected)?
}
Value::List { vals, .. } => toml::Value::Array(toml_list(engine_state, vals)?),
Value::Closure { .. } => {
let code = engine_state.get_span_contents(span);

View File

@ -62,10 +62,6 @@ pub fn value_to_yaml_value(v: &Value) -> Result<serde_yaml::Value, ShellError> {
}
serde_yaml::Value::Mapping(m)
}
Value::LazyRecord { val, .. } => {
let collected = val.collect()?;
value_to_yaml_value(&collected)?
}
Value::List { vals, .. } => {
let mut out = vec![];

View File

@ -1,6 +1,5 @@
use chrono::{DateTime, Local};
use nu_engine::command_prelude::*;
use nu_protocol::LazyRecord;
use std::time::{Duration, UNIX_EPOCH};
use sysinfo::{
Components, CpuRefreshKind, Disks, Networks, System, Users, MINIMUM_CPU_UPDATE_INTERVAL,
@ -32,10 +31,7 @@ impl Command for Sys {
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let span = call.span();
let ret = Value::lazy_record(Box::new(SysResult { span }), span);
Ok(ret.into_pipeline_data())
Ok(all_columns(call.head).into_pipeline_data())
}
fn examples(&self) -> Vec<Example> {
@ -64,36 +60,18 @@ pub struct SysResult {
pub span: Span,
}
impl LazyRecord<'_> for SysResult {
fn column_names(&self) -> Vec<&'static str> {
vec!["host", "cpu", "disks", "mem", "temp", "net"]
}
fn get_column_value(&self, column: &str) -> Result<Value, ShellError> {
let span = self.span;
match column {
"host" => Ok(host(span)),
"cpu" => Ok(cpu(span)),
"disks" => Ok(disks(span)),
"mem" => Ok(mem(span)),
"temp" => Ok(temp(span)),
"net" => Ok(net(span)),
_ => Err(ShellError::LazyRecordAccessFailed {
message: format!("Could not find column '{column}'"),
column_name: column.to_string(),
span,
}),
}
}
fn span(&self) -> Span {
self.span
}
fn clone_value(&self, span: Span) -> Value {
Value::lazy_record(Box::new((*self).clone()), span)
}
fn all_columns(span: Span) -> Value {
Value::record(
record! {
"host" => host(span),
"cpu" => cpu(span),
"disks" => disks(span),
"mem" => mem(span),
"temp" => temp(span),
"net" => net(span),
},
span,
)
}
pub fn trim_cstyle_null(s: String) -> String {

View File

@ -394,10 +394,6 @@ fn handle_table_command(
input.data = PipelineData::Empty;
handle_record(input, cfg, val.into_owned())
}
PipelineData::Value(Value::LazyRecord { val, .. }, ..) => {
input.data = val.collect()?.into_pipeline_data();
handle_table_command(input, cfg)
}
PipelineData::Value(Value::Error { error, .. }, ..) => {
// Propagate this error outward, so that it goes to stderr
// instead of stdout.

View File

@ -98,21 +98,6 @@ fn insert_uses_enumerate_index() {
assert_eq!(actual.out, "[[index, a, b]; [0, 7, 8], [1, 6, 8]]");
}
#[test]
fn insert_support_lazy_record() {
let actual =
nu!(r#"let x = (lazy make -c ["h"] -g {|a| $a | str upcase}); $x | insert a 10 | get a"#);
assert_eq!(actual.out, "10");
}
#[test]
fn lazy_record_test_values() {
let actual = nu!(
r#"lazy make --columns ["haskell", "futures", "nushell"] --get-value { |lazything| $lazything + "!" } | values | length"#
);
assert_eq!(actual.out, "3");
}
#[test]
fn deep_cell_path_creates_all_nested_records() {
let actual = nu!("{a: {}} | insert a.b.c 0 | get a.b.c");

View File

@ -103,13 +103,6 @@ fn update_uses_enumerate_index() {
assert_eq!(actual.out, "[[index, a]; [0, 8], [1, 8]]");
}
#[test]
fn update_support_lazy_record() {
let actual =
nu!(r#"let x = (lazy make -c ["h"] -g {|a| $a | str upcase}); $x | update h 10 | get h"#);
assert_eq!(actual.out, "10");
}
#[test]
fn list_replacement_closure() {
let actual = nu!("[1, 2] | update 1 {|i| $i + 1 } | to nuon");

View File

@ -112,17 +112,6 @@ fn upsert_past_end_of_list_stream() {
.contains("can't insert at index (the next available index is 3)"));
}
#[test]
fn upsert_support_lazy_record() {
let actual =
nu!(r#"let x = (lazy make -c ["h"] -g {|a| $a | str upcase}); $x | upsert h 10 | get h"#);
assert_eq!(actual.out, "10");
let actual =
nu!(r#"let x = (lazy make -c ["h"] -g {|a| $a | str upcase}); $x | upsert aa 10 | get aa"#);
assert_eq!(actual.out, "10");
}
#[test]
fn deep_cell_path_creates_all_nested_records() {
let actual = nu!("{a: {}} | upsert a.b.c 0 | get a.b.c");