Initial support for parse-time constants (#7436)

This commit is contained in:
Jakub Žádník 2022-12-22 00:21:03 +02:00 committed by GitHub
parent fa8629300f
commit 3a2c7900d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 788 additions and 242 deletions

View file

@ -0,0 +1,104 @@
use nu_protocol::ast::Call;
use nu_protocol::engine::{Command, EngineState, Stack};
use nu_protocol::{Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type};
#[derive(Clone)]
pub struct Const;
impl Command for Const {
fn name(&self) -> &str {
"const"
}
fn usage(&self) -> &str {
"Create a parse-time constant."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build("const")
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
.allow_variants_without_examples(true)
.required("const_name", SyntaxShape::VarWithOptType, "constant name")
.required(
"initial_value",
SyntaxShape::Keyword(b"=".to_vec(), Box::new(SyntaxShape::Expression)),
"equals sign followed by constant value",
)
.category(Category::Core)
}
fn extra_usage(&self) -> &str {
r#"This command is a parser keyword. For details, check:
https://www.nushell.sh/book/thinking_in_nu.html"#
}
fn is_parser_keyword(&self) -> bool {
true
}
fn search_terms(&self) -> Vec<&str> {
vec!["set", "let"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
let var_id = call
.positional_nth(0)
.expect("checked through parser")
.as_var()
.expect("internal error: missing variable");
if let Some(constval) = engine_state.find_constant(var_id, &[]) {
// Instead of creating a second copy of the value in the stack, we could change
// stack.get_var() to check engine_state.find_constant().
stack.add_var(var_id, constval.clone());
Ok(PipelineData::empty())
} else {
Err(ShellError::NushellFailedSpanned(
"Missing Constant".to_string(),
"constant not added by the parser".to_string(),
call.head,
))
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Create a new parse-time constant.",
example: "let x = 10",
result: None,
},
Example {
description: "Create a composite constant value",
example: "let x = { a: 10, b: 20 }",
result: None,
},
]
}
}
#[cfg(test)]
mod test {
use nu_protocol::engine::CommandType;
use super::*;
#[test]
fn test_examples() {
use crate::test_examples;
test_examples(Const {})
}
#[test]
fn test_command_type() {
assert!(matches!(Const.command_type(), CommandType::Keyword));
}
}

View file

@ -70,9 +70,8 @@ impl Command for Let {
)?
.0;
//println!("Adding: {:?} to {}", rhs, var_id);
stack.add_var(var_id, rhs.into_value(call.head));
Ok(PipelineData::empty())
}

View file

@ -2,6 +2,7 @@ mod alias;
mod ast;
mod break_;
mod commandline;
mod const_;
mod continue_;
mod debug;
mod def;
@ -40,6 +41,7 @@ pub use alias::Alias;
pub use ast::Ast;
pub use break_::Break;
pub use commandline::Commandline;
pub use const_::Const;
pub use continue_::Continue;
pub use debug::Debug;
pub use def::Def;

View file

@ -84,25 +84,8 @@ impl Command for OverlayUse {
));
};
let overlay_name = if let Some(kw_expression) = call.positional_nth(1) {
// If renamed via the 'as' keyword, use the new name as the overlay name
if let Some(new_name_expression) = kw_expression.as_keyword() {
if let Some(new_name) = new_name_expression.as_string() {
new_name
} else {
return Err(ShellError::NushellFailedSpanned(
"Wrong keyword type".to_string(),
"keyword argument not a string".to_string(),
new_name_expression.span,
));
}
} else {
return Err(ShellError::NushellFailedSpanned(
"Wrong keyword type".to_string(),
"keyword argument not a keyword".to_string(),
kw_expression.span,
));
}
let overlay_name = if let Some(name) = call.opt(engine_state, caller_stack, 1)? {
name
} else if engine_state
.find_overlay(name_arg.item.as_bytes())
.is_some()

View file

@ -32,6 +32,7 @@ pub fn create_default_context() -> EngineState {
Ast,
Break,
Commandline,
Const,
Continue,
Debug,
Def,

View file

@ -293,3 +293,25 @@ fn source_env_is_scoped() {
assert!(actual.err.contains("executable was not found"));
})
}
#[test]
fn source_env_const_file() {
Playground::setup("source_env_const_file", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"spam.nu",
r#"
let-env FOO = 'foo'
"#,
)]);
let inp = &[
r#"const file = 'spam.nu'"#,
r#"source-env $file"#,
r#"$env.FOO"#,
];
let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; ")));
assert_eq!(actual.out, "foo");
})
}

View file

@ -378,6 +378,19 @@ pub enum ParseError {
#[diagnostic(code(nu::shell::error_reading_file), url(docsrs))]
ReadingFile(String, #[label("{0}")] Span),
/// Tried assigning non-constant value to a constant
///
/// ## Resolution
///
/// Only a subset of expressions are allowed to be assigned as a constant during parsing.
#[error("Not a constant.")]
#[diagnostic(
code(nu::parser::not_a_constant),
url(docsrs),
help("Only a subset of expressions are allowed constants during parsing. Try using the 'let' command or typing the value literally.")
)]
NotAConstant(#[label = "Value is not a parse-time constant"] Span),
#[error("{0}")]
#[diagnostic()]
LabeledError(String, String, #[label("{1}")] Span),
@ -450,6 +463,7 @@ impl ParseError {
ParseError::ShellErrRedirect(s) => *s,
ParseError::ShellOutErrRedirect(s) => *s,
ParseError::UnknownOperator(_, _, s) => *s,
ParseError::NotAConstant(s) => *s,
}
}
}

View file

@ -0,0 +1,124 @@
use crate::ParseError;
use nu_protocol::{
ast::{Expr, Expression},
engine::StateWorkingSet,
Span, Value,
};
/// Evaluate a constant value at parse time
///
/// Based off eval_expression() in the engine
pub fn eval_constant(
working_set: &StateWorkingSet,
expr: &Expression,
) -> Result<Value, ParseError> {
match &expr.expr {
Expr::Bool(b) => Ok(Value::boolean(*b, expr.span)),
Expr::Int(i) => Ok(Value::int(*i, expr.span)),
Expr::Float(f) => Ok(Value::float(*f, expr.span)),
Expr::Binary(b) => Ok(Value::Binary {
val: b.clone(),
span: expr.span,
}),
Expr::Var(var_id) => match working_set.find_constant(*var_id) {
Some(val) => Ok(val.clone()),
None => Err(ParseError::NotAConstant(expr.span)),
},
Expr::CellPath(cell_path) => Ok(Value::CellPath {
val: cell_path.clone(),
span: expr.span,
}),
Expr::FullCellPath(cell_path) => {
let value = eval_constant(working_set, &cell_path.head)?;
match value.follow_cell_path(&cell_path.tail, false) {
Ok(val) => Ok(val),
// TODO: Better error conversion
Err(shell_error) => Err(ParseError::LabeledError(
"Error when following cell path".to_string(),
format!("{:?}", shell_error),
expr.span,
)),
}
}
Expr::DateTime(dt) => Ok(Value::Date {
val: *dt,
span: expr.span,
}),
Expr::List(x) => {
let mut output = vec![];
for expr in x {
output.push(eval_constant(working_set, expr)?);
}
Ok(Value::List {
vals: output,
span: expr.span,
})
}
Expr::Record(fields) => {
let mut cols = vec![];
let mut vals = vec![];
for (col, val) in fields {
// avoid duplicate cols.
let col_name = value_as_string(eval_constant(working_set, col)?, expr.span)?;
let pos = cols.iter().position(|c| c == &col_name);
match pos {
Some(index) => {
vals[index] = eval_constant(working_set, val)?;
}
None => {
cols.push(col_name);
vals.push(eval_constant(working_set, val)?);
}
}
}
Ok(Value::Record {
cols,
vals,
span: expr.span,
})
}
Expr::Table(headers, vals) => {
let mut output_headers = vec![];
for expr in headers {
output_headers.push(value_as_string(
eval_constant(working_set, expr)?,
expr.span,
)?);
}
let mut output_rows = vec![];
for val in vals {
let mut row = vec![];
for expr in val {
row.push(eval_constant(working_set, expr)?);
}
output_rows.push(Value::Record {
cols: output_headers.clone(),
vals: row,
span: expr.span,
});
}
Ok(Value::List {
vals: output_rows,
span: expr.span,
})
}
Expr::Keyword(_, _, expr) => eval_constant(working_set, expr),
Expr::String(s) => Ok(Value::String {
val: s.clone(),
span: expr.span,
}),
Expr::Nothing => Ok(Value::Nothing { span: expr.span }),
_ => Err(ParseError::NotAConstant(expr.span)),
}
}
/// Get the value as a string
pub fn value_as_string(value: Value, span: Span) -> Result<String, ParseError> {
match value {
Value::String { val, .. } => Ok(val),
_ => Err(ParseError::NotAConstant(span)),
}
}

View file

@ -1,5 +1,6 @@
mod deparse;
mod errors;
mod eval;
mod flatten;
mod known_external;
mod lex;

View file

@ -1,3 +1,4 @@
use crate::eval::{eval_constant, value_as_string};
use log::trace;
use nu_path::canonicalize_with;
use nu_protocol::{
@ -20,8 +21,8 @@ use crate::{
lex,
parser::{
check_call, check_name, garbage, garbage_pipeline, lite_parse, parse, parse_internal_call,
parse_multispan_value, parse_signature, parse_string, parse_var_with_opt_type, trim_quotes,
LiteCommand, LiteElement, ParsedInternalCall,
parse_multispan_value, parse_signature, parse_string, parse_value, parse_var_with_opt_type,
trim_quotes, LiteCommand, LiteElement, ParsedInternalCall,
},
unescape_unquote_string, ParseError,
};
@ -1636,144 +1637,143 @@ pub fn parse_use(
// TODO: Add checking for importing too long import patterns, e.g.:
// > use spam foo non existent names here do not throw error
let (import_pattern, module) =
if let Some(module_id) = working_set.find_module(&import_pattern.head.name) {
(import_pattern, working_set.get_module(module_id).clone())
} else {
// It could be a file
// TODO: Do not close over when loading module from file?
let (import_pattern, module) = if let Some(module_id) = import_pattern.head.id {
(import_pattern, working_set.get_module(module_id).clone())
} else {
// It could be a file
// TODO: Do not close over when loading module from file?
let (module_filename, err) =
unescape_unquote_string(&import_pattern.head.name, import_pattern.head.span);
let (module_filename, err) =
unescape_unquote_string(&import_pattern.head.name, import_pattern.head.span);
if err.is_none() {
if let Some(module_path) =
find_in_dirs(&module_filename, working_set, &cwd, LIB_DIRS_ENV)
if err.is_none() {
if let Some(module_path) =
find_in_dirs(&module_filename, working_set, &cwd, LIB_DIRS_ENV)
{
if let Some(i) = working_set
.parsed_module_files
.iter()
.rposition(|p| p == &module_path)
{
if let Some(i) = working_set
let mut files: Vec<String> = working_set
.parsed_module_files
.split_off(i)
.iter()
.rposition(|p| p == &module_path)
{
let mut files: Vec<String> = working_set
.parsed_module_files
.split_off(i)
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
.map(|p| p.to_string_lossy().to_string())
.collect();
files.push(module_path.to_string_lossy().to_string());
files.push(module_path.to_string_lossy().to_string());
let msg = files.join("\nuses ");
let msg = files.join("\nuses ");
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: call_span,
ty: Type::Any,
custom_completion: None,
}]),
vec![],
Some(ParseError::CyclicalModuleImport(
msg,
import_pattern.head.span,
)),
);
}
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: call_span,
ty: Type::Any,
custom_completion: None,
}]),
vec![],
Some(ParseError::CyclicalModuleImport(
msg,
import_pattern.head.span,
)),
);
}
let module_name = if let Some(stem) = module_path.file_stem() {
stem.to_string_lossy().to_string()
let module_name = if let Some(stem) = module_path.file_stem() {
stem.to_string_lossy().to_string()
} else {
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: call_span,
ty: Type::Any,
custom_completion: None,
}]),
vec![],
Some(ParseError::ModuleNotFound(import_pattern.head.span)),
);
};
if let Ok(contents) = std::fs::read(&module_path) {
let span_start = working_set.next_span_start();
working_set.add_file(module_filename, &contents);
let span_end = working_set.next_span_start();
// Change the currently parsed directory
let prev_currently_parsed_cwd = if let Some(parent) = module_path.parent() {
let prev = working_set.currently_parsed_cwd.clone();
working_set.currently_parsed_cwd = Some(parent.into());
prev
} else {
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: call_span,
ty: Type::Any,
custom_completion: None,
}]),
vec![],
Some(ParseError::ModuleNotFound(import_pattern.head.span)),
);
working_set.currently_parsed_cwd.clone()
};
if let Ok(contents) = std::fs::read(&module_path) {
let span_start = working_set.next_span_start();
working_set.add_file(module_filename, &contents);
let span_end = working_set.next_span_start();
// Add the file to the stack of parsed module files
working_set.parsed_module_files.push(module_path);
// Change the currently parsed directory
let prev_currently_parsed_cwd = if let Some(parent) = module_path.parent() {
let prev = working_set.currently_parsed_cwd.clone();
// Parse the module
let (block, module, err) = parse_module_block(
working_set,
Span::new(span_start, span_end),
expand_aliases_denylist,
);
error = error.or(err);
working_set.currently_parsed_cwd = Some(parent.into());
// Remove the file from the stack of parsed module files
working_set.parsed_module_files.pop();
prev
} else {
working_set.currently_parsed_cwd.clone()
};
// Restore the currently parsed directory back
working_set.currently_parsed_cwd = prev_currently_parsed_cwd;
// Add the file to the stack of parsed module files
working_set.parsed_module_files.push(module_path);
let _ = working_set.add_block(block);
let module_id = working_set.add_module(&module_name, module.clone());
// Parse the module
let (block, module, err) = parse_module_block(
working_set,
Span::new(span_start, span_end),
expand_aliases_denylist,
);
error = error.or(err);
// Remove the file from the stack of parsed module files
working_set.parsed_module_files.pop();
// Restore the currently parsed directory back
working_set.currently_parsed_cwd = prev_currently_parsed_cwd;
let _ = working_set.add_block(block);
let module_id = working_set.add_module(&module_name, module.clone());
(
ImportPattern {
head: ImportPatternHead {
name: module_name.into(),
id: Some(module_id),
span: import_pattern.head.span,
},
members: import_pattern.members,
hidden: HashSet::new(),
(
ImportPattern {
head: ImportPatternHead {
name: module_name.into(),
id: Some(module_id),
span: import_pattern.head.span,
},
module,
)
} else {
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: call_span,
ty: Type::Any,
custom_completion: None,
}]),
vec![],
Some(ParseError::ModuleNotFound(import_pattern.head.span)),
);
}
members: import_pattern.members,
hidden: HashSet::new(),
},
module,
)
} else {
error = error.or(Some(ParseError::ModuleNotFound(import_pattern.head.span)));
let old_span = import_pattern.head.span;
let mut import_pattern = ImportPattern::new();
import_pattern.head.span = old_span;
(import_pattern, Module::new())
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: call_span,
ty: Type::Any,
custom_completion: None,
}]),
vec![],
Some(ParseError::ModuleNotFound(import_pattern.head.span)),
);
}
} else {
return (
garbage_pipeline(spans),
vec![],
Some(ParseError::NonUtf8(import_pattern.head.span)),
);
error = error.or(Some(ParseError::ModuleNotFound(import_pattern.head.span)));
let old_span = import_pattern.head.span;
let mut import_pattern = ImportPattern::new();
import_pattern.head.span = old_span;
(import_pattern, Module::new())
}
};
} else {
return (
garbage_pipeline(spans),
vec![],
Some(ParseError::NonUtf8(import_pattern.head.span)),
);
}
};
let (decls_to_use, aliases_to_use) = if import_pattern.members.is_empty() {
(
@ -2386,16 +2386,16 @@ pub fn parse_overlay_use(
};
let (overlay_name, overlay_name_span) = if let Some(expr) = call.positional_nth(0) {
if let Some(s) = expr.as_string() {
(s, expr.span)
} else {
return (
garbage_pipeline(spans),
Some(ParseError::UnknownState(
"internal error: Module name not a string".into(),
expr.span,
)),
);
match eval_constant(working_set, expr) {
Ok(val) => match value_as_string(val, expr.span) {
Ok(s) => (s, expr.span),
Err(err) => {
return (garbage_pipeline(spans), Some(err));
}
},
Err(err) => {
return (garbage_pipeline(spans), Some(err));
}
}
} else {
return (
@ -2409,20 +2409,15 @@ pub fn parse_overlay_use(
let new_name = if let Some(kw_expression) = call.positional_nth(1) {
if let Some(new_name_expression) = kw_expression.as_keyword() {
if let Some(new_name) = new_name_expression.as_string() {
Some(Spanned {
item: new_name,
span: new_name_expression.span,
})
} else {
return (
garbage_pipeline(spans),
Some(ParseError::TypeMismatch(
Type::String,
new_name_expression.ty.clone(),
new_name_expression.span,
)),
);
match eval_constant(working_set, new_name_expression) {
Ok(val) => match value_as_string(val, new_name_expression.span) {
Ok(s) => Some(Spanned {
item: s,
span: new_name_expression.span,
}),
Err(err) => return (garbage_pipeline(spans), Some(err)),
},
Err(err) => return (garbage_pipeline(spans), Some(err)),
}
} else {
return (
@ -2751,19 +2746,23 @@ pub fn parse_overlay_hide(
(pipeline, None)
}
pub fn parse_let(
pub fn parse_let_or_const(
working_set: &mut StateWorkingSet,
spans: &[Span],
expand_aliases_denylist: &[usize],
) -> (Pipeline, Option<ParseError>) {
let name = working_set.get_span_contents(spans[0]);
if name == b"let" {
if name == b"let" || name == b"const" {
let is_const = &name == b"const";
if let Some((span, err)) = check_name(working_set, spans) {
return (Pipeline::from_vec(vec![garbage(*span)]), Some(err));
}
if let Some(decl_id) = working_set.find_decl(b"let", &Type::Any) {
if let Some(decl_id) =
working_set.find_decl(if is_const { b"const" } else { b"let" }, &Type::Any)
{
let cmd = working_set.get_decl(decl_id);
let call_signature = cmd.signature().call_signature();
@ -2815,6 +2814,15 @@ pub fn parse_let(
if let Some(var_id) = var_id {
working_set.set_variable_type(var_id, rhs_type);
if is_const {
match eval_constant(working_set, &rvalue) {
Ok(val) => {
working_set.add_constant(var_id, val);
}
Err(err) => error = error.or(Some(err)),
}
}
}
let call = Box::new(Call {
@ -2866,7 +2874,7 @@ pub fn parse_let(
(
garbage_pipeline(spans),
Some(ParseError::UnknownState(
"internal error: let statement unparseable".into(),
"internal error: let or const statement unparseable".into(),
span(spans),
)),
)
@ -3036,79 +3044,111 @@ pub fn parse_source(
// Command and one file name
if spans.len() >= 2 {
let name_expr = working_set.get_span_contents(spans[1]);
let (filename, err) = unescape_unquote_string(name_expr, spans[1]);
let (expr, err) = parse_value(
working_set,
spans[1],
&SyntaxShape::Any,
expand_aliases_denylist,
);
if err.is_none() {
if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_ENV) {
if let Ok(contents) = std::fs::read(&path) {
// Change currently parsed directory
let prev_currently_parsed_cwd = if let Some(parent) = path.parent() {
let prev = working_set.currently_parsed_cwd.clone();
error = error.or(err);
working_set.currently_parsed_cwd = Some(parent.into());
let val = match eval_constant(working_set, &expr) {
Ok(val) => val,
Err(err) => {
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: span(&spans[1..]),
ty: Type::Any,
custom_completion: None,
}]),
Some(err),
);
}
};
prev
} else {
working_set.currently_parsed_cwd.clone()
};
let filename = match value_as_string(val, spans[1]) {
Ok(s) => s,
Err(err) => {
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: span(&spans[1..]),
ty: Type::Any,
custom_completion: None,
}]),
Some(err),
);
}
};
// This will load the defs from the file into the
// working set, if it was a successful parse.
let (block, err) = parse(
working_set,
path.file_name().and_then(|x| x.to_str()),
&contents,
scoped,
expand_aliases_denylist,
);
if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_ENV) {
if let Ok(contents) = std::fs::read(&path) {
// Change currently parsed directory
let prev_currently_parsed_cwd = if let Some(parent) = path.parent() {
let prev = working_set.currently_parsed_cwd.clone();
// Restore the currently parsed directory back
working_set.currently_parsed_cwd = prev_currently_parsed_cwd;
working_set.currently_parsed_cwd = Some(parent.into());
if err.is_some() {
// Unsuccessful parse of file
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: span(&spans[1..]),
ty: Type::Any,
custom_completion: None,
}]),
// Return the file parse error
err,
);
} else {
// Save the block into the working set
let block_id = working_set.add_block(block);
prev
} else {
working_set.currently_parsed_cwd.clone()
};
let mut call_with_block = call;
// This will load the defs from the file into the
// working set, if it was a successful parse.
let (block, err) = parse(
working_set,
path.file_name().and_then(|x| x.to_str()),
&contents,
scoped,
expand_aliases_denylist,
);
// FIXME: Adding this expression to the positional creates a syntax highlighting error
// after writing `source example.nu`
call_with_block.add_positional(Expression {
expr: Expr::Int(block_id as i64),
span: spans[1],
// Restore the currently parsed directory back
working_set.currently_parsed_cwd = prev_currently_parsed_cwd;
if err.is_some() {
// Unsuccessful parse of file
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call),
span: span(&spans[1..]),
ty: Type::Any,
custom_completion: None,
});
}]),
// Return the file parse error
err,
);
} else {
// Save the block into the working set
let block_id = working_set.add_block(block);
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call_with_block),
span: span(spans),
ty: Type::Any,
custom_completion: None,
}]),
None,
);
}
let mut call_with_block = call;
// FIXME: Adding this expression to the positional creates a syntax highlighting error
// after writing `source example.nu`
call_with_block.add_positional(Expression {
expr: Expr::Int(block_id as i64),
span: spans[1],
ty: Type::Any,
custom_completion: None,
});
return (
Pipeline::from_vec(vec![Expression {
expr: Expr::Call(call_with_block),
span: span(spans),
ty: Type::Any,
custom_completion: None,
}]),
None,
);
}
} else {
error = error.or(Some(ParseError::SourcedFileNotFound(filename, spans[1])));
}
} else {
return (garbage_pipeline(spans), Some(ParseError::NonUtf8(spans[1])));
error = error.or(Some(ParseError::SourcedFileNotFound(filename, spans[1])));
}
}
return (

View file

@ -1,4 +1,5 @@
use crate::{
eval::{eval_constant, value_as_string},
lex, parse_mut,
type_check::{math_result_type, type_compatible},
ParseError, Token, TokenContents,
@ -17,8 +18,8 @@ use nu_protocol::{
use crate::parse_keywords::{
parse_alias, parse_def, parse_def_predecl, parse_export_in_block, parse_extern, parse_for,
parse_hide, parse_let, parse_module, parse_overlay, parse_source, parse_use, parse_where,
parse_where_expr,
parse_hide, parse_let_or_const, parse_module, parse_overlay, parse_source, parse_use,
parse_where, parse_where_expr,
};
use itertools::Itertools;
@ -2832,11 +2833,8 @@ pub fn parse_import_pattern(
) -> (Expression, Option<ParseError>) {
let mut error = None;
let (head, head_span) = if let Some(head_span) = spans.get(0) {
(
working_set.get_span_contents(*head_span).to_vec(),
head_span,
)
let head_span = if let Some(head_span) = spans.get(0) {
head_span
} else {
return (
garbage(span(spans)),
@ -2844,7 +2842,25 @@ pub fn parse_import_pattern(
);
};
let maybe_module_id = working_set.find_module(&head);
let (head_expr, err) = parse_value(
working_set,
*head_span,
&SyntaxShape::Any,
expand_aliases_denylist,
);
error = error.or(err);
let (maybe_module_id, head_name) = match eval_constant(working_set, &head_expr) {
Ok(val) => match value_as_string(val, head_expr.span) {
Ok(s) => (working_set.find_module(s.as_bytes()), s.into_bytes()),
Err(err) => {
return (garbage(span(spans)), error.or(Some(err)));
}
},
Err(err) => {
return (garbage(span(spans)), error.or(Some(err)));
}
};
let (import_pattern, err) = if let Some(tail_span) = spans.get(1) {
// FIXME: expand this to handle deeper imports once we support module imports
@ -2853,7 +2869,7 @@ pub fn parse_import_pattern(
(
ImportPattern {
head: ImportPatternHead {
name: head,
name: head_name,
id: maybe_module_id,
span: *head_span,
},
@ -2886,7 +2902,7 @@ pub fn parse_import_pattern(
(
ImportPattern {
head: ImportPatternHead {
name: head,
name: head_name,
id: maybe_module_id,
span: *head_span,
},
@ -2899,7 +2915,7 @@ pub fn parse_import_pattern(
_ => (
ImportPattern {
head: ImportPatternHead {
name: head,
name: head_name,
id: maybe_module_id,
span: *head_span,
},
@ -2914,7 +2930,7 @@ pub fn parse_import_pattern(
(
ImportPattern {
head: ImportPatternHead {
name: head,
name: head_name,
id: maybe_module_id,
span: *head_span,
},
@ -2931,7 +2947,7 @@ pub fn parse_import_pattern(
(
ImportPattern {
head: ImportPatternHead {
name: head,
name: head_name,
id: maybe_module_id,
span: *head_span,
},
@ -4898,7 +4914,7 @@ pub fn parse_expression(
.0,
Some(ParseError::BuiltinCommandInPipeline("for".into(), spans[0])),
),
b"let" => (
b"let" | b"const" => (
parse_call(
working_set,
&spans[pos..],
@ -5166,7 +5182,9 @@ pub fn parse_builtin_commands(
match name {
b"def" | b"def-env" => parse_def(working_set, lite_command, expand_aliases_denylist),
b"extern" => parse_extern(working_set, lite_command, expand_aliases_denylist),
b"let" => parse_let(working_set, &lite_command.parts, expand_aliases_denylist),
b"let" | b"const" => {
parse_let_or_const(working_set, &lite_command.parts, expand_aliases_denylist)
}
b"mut" => parse_mut(working_set, &lite_command.parts, expand_aliases_denylist),
b"for" => {
let (expr, err) = parse_for(working_set, &lite_command.parts, expand_aliases_denylist);

View file

@ -173,6 +173,9 @@ impl EngineState {
for item in delta_overlay.vars.into_iter() {
existing_overlay.vars.insert(item.0, item.1);
}
for item in delta_overlay.constants.into_iter() {
existing_overlay.constants.insert(item.0, item.1);
}
for item in delta_overlay.aliases.into_iter() {
existing_overlay.aliases.insert(item.0, item.1);
}
@ -626,6 +629,16 @@ impl EngineState {
output
}
pub fn find_constant(&self, var_id: VarId, removed_overlays: &[Vec<u8>]) -> Option<&Value> {
for overlay_frame in self.active_overlays(removed_overlays).iter().rev() {
if let Some(val) = overlay_frame.constants.get(&var_id) {
return Some(val);
}
}
None
}
pub fn get_span_contents(&self, span: &Span) -> &[u8] {
for (contents, start, finish) in &self.file_contents {
if span.start >= *start && span.end <= *finish {
@ -1654,6 +1667,29 @@ impl<'a> StateWorkingSet<'a> {
}
}
pub fn add_constant(&mut self, var_id: VarId, val: Value) {
self.last_overlay_mut().constants.insert(var_id, val);
}
pub fn find_constant(&self, var_id: VarId) -> Option<&Value> {
let mut removed_overlays = vec![];
for scope_frame in self.delta.scope.iter().rev() {
for overlay_frame in scope_frame
.active_overlays(&mut removed_overlays)
.iter()
.rev()
{
if let Some(val) = overlay_frame.constants.get(&var_id) {
return Some(val);
}
}
}
self.permanent_state
.find_constant(var_id, &removed_overlays)
}
pub fn get_variable(&self, var_id: VarId) -> &Variable {
let num_permanent_vars = self.permanent_state.num_vars();
if var_id < num_permanent_vars {

View file

@ -1,4 +1,4 @@
use crate::{AliasId, DeclId, ModuleId, OverlayId, Type, VarId};
use crate::{AliasId, DeclId, ModuleId, OverlayId, Type, Value, VarId};
use std::borrow::Borrow;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
@ -199,6 +199,7 @@ impl ScopeFrame {
#[derive(Debug, Clone)]
pub struct OverlayFrame {
pub vars: HashMap<Vec<u8>, VarId>,
pub constants: HashMap<VarId, Value>,
pub predecls: HashMap<Vec<u8>, DeclId>, // temporary storage for predeclarations
pub decls: HashMap<(Vec<u8>, Type), DeclId>,
pub aliases: HashMap<Vec<u8>, AliasId>,
@ -212,6 +213,7 @@ impl OverlayFrame {
pub fn from_origin(origin: ModuleId, prefixed: bool) -> Self {
Self {
vars: HashMap::new(),
constants: HashMap::new(),
predecls: HashMap::new(),
decls: HashMap::new(),
aliases: HashMap::new(),

103
tests/const_/mod.rs Normal file
View file

@ -0,0 +1,103 @@
use nu_test_support::{nu, pipeline};
#[test]
fn const_bool() {
let inp = &[r#"const x = false"#, r#"$x"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "false");
}
#[test]
fn const_int() {
let inp = &[r#"const x = 10"#, r#"$x"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "10");
}
#[test]
fn const_float() {
let inp = &[r#"const x = 1.234"#, r#"$x"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "1.234");
}
#[test]
fn const_binary() {
let inp = &[r#"const x = 0x[12]"#, r#"$x"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert!(actual.out.contains("12"));
}
#[test]
fn const_datetime() {
let inp = &[r#"const x = 2021-02-27T13:55:40+00:00"#, r#"$x"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert!(actual.out.contains("Sat, 27 Feb 2021 13:55:40"));
}
#[test]
fn const_list() {
let inp = &[r#"const x = [ a b c ]"#, r#"$x | describe"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "list<string>");
}
#[test]
fn const_record() {
let inp = &[r#"const x = { a: 10, b: 20, c: 30 }"#, r#"$x | describe"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "record<a: int, b: int, c: int>");
}
#[test]
fn const_table() {
let inp = &[
r#"const x = [[a b c]; [10 20 30] [100 200 300]]"#,
r#"$x | describe"#,
];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "table<a: int, b: int, c: int>");
}
#[test]
fn const_string() {
let inp = &[r#"const x = "abc""#, r#"$x"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "abc");
}
#[test]
fn const_nothing() {
let inp = &[r#"const x = $nothing"#, r#"$x | describe"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "nothing");
}
#[test]
fn const_unsupported() {
let inp = &[r#"const x = ('abc' | str length)"#];
let actual = nu!(cwd: "tests/const_", pipeline(&inp.join("; ")));
assert!(actual.err.contains("not_a_constant"));
}

View file

@ -1,5 +1,6 @@
extern crate nu_test_support;
mod const_;
mod hooks;
mod modules;
mod overlays;

View file

@ -433,3 +433,44 @@ fn module_cyclical_imports_3() {
assert!(actual.err.contains("cyclical"));
})
}
#[test]
fn module_import_const_file() {
Playground::setup("module_import_const_file", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"spam.nu",
r#"
export def foo [] { "foo" }
"#,
)]);
let inp = &[r#"const file = 'spam.nu'"#, r#"use $file foo"#, r#"foo"#];
let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; ")));
assert_eq!(actual.out, "foo");
})
}
#[test]
fn module_import_const_module_name() {
Playground::setup("module_import_const_file", |dirs, sandbox| {
sandbox.with_files(vec![FileWithContentToBeTrimmed(
"spam.nu",
r#"
export def foo [] { "foo" }
"#,
)]);
let inp = &[
r#"module spam { export def foo [] { "foo" } }"#,
r#"const mod = 'spam'"#,
r#"use $mod foo"#,
r#"foo"#,
];
let actual = nu!(cwd: dirs.test(), pipeline(&inp.join("; ")));
assert_eq!(actual.out, "foo");
})
}

View file

@ -169,6 +169,33 @@ fn add_overlay_from_file_decl() {
assert_eq!(actual_repl.out, "foo");
}
#[test]
fn add_overlay_from_const_file_decl() {
let inp = &[
r#"const file = 'samples/spam.nu'"#,
r#"overlay use $file"#,
r#"foo"#,
];
let actual = nu!(cwd: "tests/overlays", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "foo");
}
#[test]
fn add_overlay_from_const_module_name_decl() {
let inp = &[
r#"module spam { export def foo [] { "foo" } }"#,
r#"const mod = 'spam'"#,
r#"overlay use $mod"#,
r#"foo"#,
];
let actual = nu!(cwd: "tests/overlays", pipeline(&inp.join("; ")));
assert_eq!(actual.out, "foo");
}
// This one tests that the `nu_repl()` loop works correctly
#[test]
fn add_overlay_from_file_decl_cd() {
@ -693,6 +720,23 @@ fn overlay_add_renamed() {
assert_eq!(actual_repl.out, "foo");
}
#[test]
fn overlay_add_renamed_const() {
let inp = &[
r#"module spam { export def foo [] { "foo" } }"#,
r#"const name = 'spam'"#,
r#"const new_name = 'eggs'"#,
r#"overlay use $name as $new_name --prefix"#,
r#"eggs foo"#,
];
let actual = nu!(cwd: "tests/overlays", pipeline(&inp.join("; ")));
let actual_repl = nu!(cwd: "tests/overlays", nu_repl_code(inp));
assert_eq!(actual.out, "foo");
assert_eq!(actual_repl.out, "foo");
}
#[test]
fn overlay_add_renamed_from_file() {
let inp = &[

View file

@ -11,6 +11,17 @@ fn source_file_relative_to_file() {
assert_eq!(actual.out, "5");
}
#[test]
fn source_const_file() {
let actual = nu!(cwd: "tests/parsing/samples",
r#"
const file = 'single_line.nu'
source $file
"#);
assert_eq!(actual.out, "5");
}
#[test]
fn run_nu_script_single_line() {
let actual = nu!(cwd: "tests/parsing/samples", r#"