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; .0;
//println!("Adding: {:?} to {}", rhs, var_id);
stack.add_var(var_id, rhs.into_value(call.head)); stack.add_var(var_id, rhs.into_value(call.head));
Ok(PipelineData::empty()) Ok(PipelineData::empty())
} }

View File

@ -2,6 +2,7 @@ mod alias;
mod ast; mod ast;
mod break_; mod break_;
mod commandline; mod commandline;
mod const_;
mod continue_; mod continue_;
mod debug; mod debug;
mod def; mod def;
@ -40,6 +41,7 @@ pub use alias::Alias;
pub use ast::Ast; pub use ast::Ast;
pub use break_::Break; pub use break_::Break;
pub use commandline::Commandline; pub use commandline::Commandline;
pub use const_::Const;
pub use continue_::Continue; pub use continue_::Continue;
pub use debug::Debug; pub use debug::Debug;
pub use def::Def; 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) { let overlay_name = if let Some(name) = call.opt(engine_state, caller_stack, 1)? {
// If renamed via the 'as' keyword, use the new name as the overlay name 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,
));
}
} else if engine_state } else if engine_state
.find_overlay(name_arg.item.as_bytes()) .find_overlay(name_arg.item.as_bytes())
.is_some() .is_some()

View File

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

View File

@ -293,3 +293,25 @@ fn source_env_is_scoped() {
assert!(actual.err.contains("executable was not found")); 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))] #[diagnostic(code(nu::shell::error_reading_file), url(docsrs))]
ReadingFile(String, #[label("{0}")] Span), 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}")] #[error("{0}")]
#[diagnostic()] #[diagnostic()]
LabeledError(String, String, #[label("{1}")] Span), LabeledError(String, String, #[label("{1}")] Span),
@ -450,6 +463,7 @@ impl ParseError {
ParseError::ShellErrRedirect(s) => *s, ParseError::ShellErrRedirect(s) => *s,
ParseError::ShellOutErrRedirect(s) => *s, ParseError::ShellOutErrRedirect(s) => *s,
ParseError::UnknownOperator(_, _, 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 deparse;
mod errors; mod errors;
mod eval;
mod flatten; mod flatten;
mod known_external; mod known_external;
mod lex; mod lex;

View File

@ -1,3 +1,4 @@
use crate::eval::{eval_constant, value_as_string};
use log::trace; use log::trace;
use nu_path::canonicalize_with; use nu_path::canonicalize_with;
use nu_protocol::{ use nu_protocol::{
@ -20,8 +21,8 @@ use crate::{
lex, lex,
parser::{ parser::{
check_call, check_name, garbage, garbage_pipeline, lite_parse, parse, parse_internal_call, 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, parse_multispan_value, parse_signature, parse_string, parse_value, parse_var_with_opt_type,
LiteCommand, LiteElement, ParsedInternalCall, trim_quotes, LiteCommand, LiteElement, ParsedInternalCall,
}, },
unescape_unquote_string, ParseError, unescape_unquote_string, ParseError,
}; };
@ -1636,8 +1637,7 @@ pub fn parse_use(
// TODO: Add checking for importing too long import patterns, e.g.: // TODO: Add checking for importing too long import patterns, e.g.:
// > use spam foo non existent names here do not throw error // > use spam foo non existent names here do not throw error
let (import_pattern, module) = let (import_pattern, module) = if let Some(module_id) = import_pattern.head.id {
if let Some(module_id) = working_set.find_module(&import_pattern.head.name) {
(import_pattern, working_set.get_module(module_id).clone()) (import_pattern, working_set.get_module(module_id).clone())
} else { } else {
// It could be a file // It could be a file
@ -2386,16 +2386,16 @@ pub fn parse_overlay_use(
}; };
let (overlay_name, overlay_name_span) = if let Some(expr) = call.positional_nth(0) { let (overlay_name, overlay_name_span) = if let Some(expr) = call.positional_nth(0) {
if let Some(s) = expr.as_string() { match eval_constant(working_set, expr) {
(s, expr.span) Ok(val) => match value_as_string(val, expr.span) {
} else { Ok(s) => (s, expr.span),
return ( Err(err) => {
garbage_pipeline(spans), return (garbage_pipeline(spans), Some(err));
Some(ParseError::UnknownState( }
"internal error: Module name not a string".into(), },
expr.span, Err(err) => {
)), return (garbage_pipeline(spans), Some(err));
); }
} }
} else { } else {
return ( return (
@ -2409,20 +2409,15 @@ pub fn parse_overlay_use(
let new_name = if let Some(kw_expression) = call.positional_nth(1) { 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_expression) = kw_expression.as_keyword() {
if let Some(new_name) = new_name_expression.as_string() { match eval_constant(working_set, new_name_expression) {
Some(Spanned { Ok(val) => match value_as_string(val, new_name_expression.span) {
item: new_name, Ok(s) => Some(Spanned {
item: s,
span: new_name_expression.span, span: new_name_expression.span,
}) }),
} else { Err(err) => return (garbage_pipeline(spans), Some(err)),
return ( },
garbage_pipeline(spans), Err(err) => return (garbage_pipeline(spans), Some(err)),
Some(ParseError::TypeMismatch(
Type::String,
new_name_expression.ty.clone(),
new_name_expression.span,
)),
);
} }
} else { } else {
return ( return (
@ -2751,19 +2746,23 @@ pub fn parse_overlay_hide(
(pipeline, None) (pipeline, None)
} }
pub fn parse_let( pub fn parse_let_or_const(
working_set: &mut StateWorkingSet, working_set: &mut StateWorkingSet,
spans: &[Span], spans: &[Span],
expand_aliases_denylist: &[usize], expand_aliases_denylist: &[usize],
) -> (Pipeline, Option<ParseError>) { ) -> (Pipeline, Option<ParseError>) {
let name = working_set.get_span_contents(spans[0]); 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) { if let Some((span, err)) = check_name(working_set, spans) {
return (Pipeline::from_vec(vec![garbage(*span)]), Some(err)); 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 cmd = working_set.get_decl(decl_id);
let call_signature = cmd.signature().call_signature(); let call_signature = cmd.signature().call_signature();
@ -2815,6 +2814,15 @@ pub fn parse_let(
if let Some(var_id) = var_id { if let Some(var_id) = var_id {
working_set.set_variable_type(var_id, rhs_type); 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 { let call = Box::new(Call {
@ -2866,7 +2874,7 @@ pub fn parse_let(
( (
garbage_pipeline(spans), garbage_pipeline(spans),
Some(ParseError::UnknownState( Some(ParseError::UnknownState(
"internal error: let statement unparseable".into(), "internal error: let or const statement unparseable".into(),
span(spans), span(spans),
)), )),
) )
@ -3036,10 +3044,45 @@ pub fn parse_source(
// Command and one file name // Command and one file name
if spans.len() >= 2 { if spans.len() >= 2 {
let name_expr = working_set.get_span_contents(spans[1]); let (expr, err) = parse_value(
let (filename, err) = unescape_unquote_string(name_expr, spans[1]); working_set,
spans[1],
&SyntaxShape::Any,
expand_aliases_denylist,
);
error = error.or(err);
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),
);
}
};
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),
);
}
};
if err.is_none() {
if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_ENV) { if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_ENV) {
if let Ok(contents) = std::fs::read(&path) { if let Ok(contents) = std::fs::read(&path) {
// Change currently parsed directory // Change currently parsed directory
@ -3107,9 +3150,6 @@ pub fn parse_source(
} else { } else {
error = error.or(Some(ParseError::SourcedFileNotFound(filename, spans[1]))); error = error.or(Some(ParseError::SourcedFileNotFound(filename, spans[1])));
} }
} else {
return (garbage_pipeline(spans), Some(ParseError::NonUtf8(spans[1])));
}
} }
return ( return (
Pipeline::from_vec(vec![Expression { Pipeline::from_vec(vec![Expression {

View File

@ -1,4 +1,5 @@
use crate::{ use crate::{
eval::{eval_constant, value_as_string},
lex, parse_mut, lex, parse_mut,
type_check::{math_result_type, type_compatible}, type_check::{math_result_type, type_compatible},
ParseError, Token, TokenContents, ParseError, Token, TokenContents,
@ -17,8 +18,8 @@ use nu_protocol::{
use crate::parse_keywords::{ use crate::parse_keywords::{
parse_alias, parse_def, parse_def_predecl, parse_export_in_block, parse_extern, parse_for, 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_hide, parse_let_or_const, parse_module, parse_overlay, parse_source, parse_use,
parse_where_expr, parse_where, parse_where_expr,
}; };
use itertools::Itertools; use itertools::Itertools;
@ -2832,11 +2833,8 @@ pub fn parse_import_pattern(
) -> (Expression, Option<ParseError>) { ) -> (Expression, Option<ParseError>) {
let mut error = None; let mut error = None;
let (head, head_span) = if let Some(head_span) = spans.get(0) { let head_span = if let Some(head_span) = spans.get(0) {
( head_span
working_set.get_span_contents(*head_span).to_vec(),
head_span,
)
} else { } else {
return ( return (
garbage(span(spans)), 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) { let (import_pattern, err) = if let Some(tail_span) = spans.get(1) {
// FIXME: expand this to handle deeper imports once we support module imports // FIXME: expand this to handle deeper imports once we support module imports
@ -2853,7 +2869,7 @@ pub fn parse_import_pattern(
( (
ImportPattern { ImportPattern {
head: ImportPatternHead { head: ImportPatternHead {
name: head, name: head_name,
id: maybe_module_id, id: maybe_module_id,
span: *head_span, span: *head_span,
}, },
@ -2886,7 +2902,7 @@ pub fn parse_import_pattern(
( (
ImportPattern { ImportPattern {
head: ImportPatternHead { head: ImportPatternHead {
name: head, name: head_name,
id: maybe_module_id, id: maybe_module_id,
span: *head_span, span: *head_span,
}, },
@ -2899,7 +2915,7 @@ pub fn parse_import_pattern(
_ => ( _ => (
ImportPattern { ImportPattern {
head: ImportPatternHead { head: ImportPatternHead {
name: head, name: head_name,
id: maybe_module_id, id: maybe_module_id,
span: *head_span, span: *head_span,
}, },
@ -2914,7 +2930,7 @@ pub fn parse_import_pattern(
( (
ImportPattern { ImportPattern {
head: ImportPatternHead { head: ImportPatternHead {
name: head, name: head_name,
id: maybe_module_id, id: maybe_module_id,
span: *head_span, span: *head_span,
}, },
@ -2931,7 +2947,7 @@ pub fn parse_import_pattern(
( (
ImportPattern { ImportPattern {
head: ImportPatternHead { head: ImportPatternHead {
name: head, name: head_name,
id: maybe_module_id, id: maybe_module_id,
span: *head_span, span: *head_span,
}, },
@ -4898,7 +4914,7 @@ pub fn parse_expression(
.0, .0,
Some(ParseError::BuiltinCommandInPipeline("for".into(), spans[0])), Some(ParseError::BuiltinCommandInPipeline("for".into(), spans[0])),
), ),
b"let" => ( b"let" | b"const" => (
parse_call( parse_call(
working_set, working_set,
&spans[pos..], &spans[pos..],
@ -5166,7 +5182,9 @@ pub fn parse_builtin_commands(
match name { match name {
b"def" | b"def-env" => parse_def(working_set, lite_command, expand_aliases_denylist), 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"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"mut" => parse_mut(working_set, &lite_command.parts, expand_aliases_denylist),
b"for" => { b"for" => {
let (expr, err) = parse_for(working_set, &lite_command.parts, expand_aliases_denylist); 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() { for item in delta_overlay.vars.into_iter() {
existing_overlay.vars.insert(item.0, item.1); 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() { for item in delta_overlay.aliases.into_iter() {
existing_overlay.aliases.insert(item.0, item.1); existing_overlay.aliases.insert(item.0, item.1);
} }
@ -626,6 +629,16 @@ impl EngineState {
output 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] { pub fn get_span_contents(&self, span: &Span) -> &[u8] {
for (contents, start, finish) in &self.file_contents { for (contents, start, finish) in &self.file_contents {
if span.start >= *start && span.end <= *finish { 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 { pub fn get_variable(&self, var_id: VarId) -> &Variable {
let num_permanent_vars = self.permanent_state.num_vars(); let num_permanent_vars = self.permanent_state.num_vars();
if var_id < num_permanent_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::borrow::Borrow;
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@ -199,6 +199,7 @@ impl ScopeFrame {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OverlayFrame { pub struct OverlayFrame {
pub vars: HashMap<Vec<u8>, VarId>, pub vars: HashMap<Vec<u8>, VarId>,
pub constants: HashMap<VarId, Value>,
pub predecls: HashMap<Vec<u8>, DeclId>, // temporary storage for predeclarations pub predecls: HashMap<Vec<u8>, DeclId>, // temporary storage for predeclarations
pub decls: HashMap<(Vec<u8>, Type), DeclId>, pub decls: HashMap<(Vec<u8>, Type), DeclId>,
pub aliases: HashMap<Vec<u8>, AliasId>, pub aliases: HashMap<Vec<u8>, AliasId>,
@ -212,6 +213,7 @@ impl OverlayFrame {
pub fn from_origin(origin: ModuleId, prefixed: bool) -> Self { pub fn from_origin(origin: ModuleId, prefixed: bool) -> Self {
Self { Self {
vars: HashMap::new(), vars: HashMap::new(),
constants: HashMap::new(),
predecls: HashMap::new(), predecls: HashMap::new(),
decls: HashMap::new(), decls: HashMap::new(),
aliases: 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; extern crate nu_test_support;
mod const_;
mod hooks; mod hooks;
mod modules; mod modules;
mod overlays; mod overlays;

View File

@ -433,3 +433,44 @@ fn module_cyclical_imports_3() {
assert!(actual.err.contains("cyclical")); 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"); 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 // This one tests that the `nu_repl()` loop works correctly
#[test] #[test]
fn add_overlay_from_file_decl_cd() { fn add_overlay_from_file_decl_cd() {
@ -693,6 +720,23 @@ fn overlay_add_renamed() {
assert_eq!(actual_repl.out, "foo"); 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] #[test]
fn overlay_add_renamed_from_file() { fn overlay_add_renamed_from_file() {
let inp = &[ let inp = &[

View File

@ -11,6 +11,17 @@ fn source_file_relative_to_file() {
assert_eq!(actual.out, "5"); 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] #[test]
fn run_nu_script_single_line() { fn run_nu_script_single_line() {
let actual = nu!(cwd: "tests/parsing/samples", r#" let actual = nu!(cwd: "tests/parsing/samples", r#"