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
18 changed files with 788 additions and 242 deletions

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);