forked from extern/nushell
Module: support defining const and use const variables inside of function (#9773)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> Relative: #8248 After this pr, user can define const variable inside a module.  And user can export const variables, the following screenshot shows how it works (it follows https://github.com/nushell/nushell/issues/8248#issuecomment-1637442612):  ## About the change 1. To make module support const, we need to change `parse_module_block` to support `const` keyword. 2. To suport export `const`, we need to make module tracking variables, so we add `variables` attribute to `Module` 3. During eval, the const variable may not exists in `stack`, because we don't eval `const` when we define a module, so we need to find variables which are already registered in `engine_state` ## One more thing to note about the const value. Consider the following code ``` module foo { const b = 3; export def bar [] { $b } } use foo bar const b = 4; bar ``` The result will be 3 (which is defined in module) rather than 4. I think it's expected behavior. It's something like [dynamic binding](https://www.gnu.org/software/emacs/manual/html_node/elisp/Dynamic-Binding-Tips.html) vs [lexical binding](https://www.gnu.org/software/emacs/manual/html_node/elisp/Lexical-Binding.html) in lisp like language, and lexical binding should be right behavior which generates more predicable result, and it doesn't introduce really subtle bugs in nushell code. What if user want dynamic-binding?(For example: the example code returns `4`) There is no way to do this, user should consider passing the value as argument to custom command rather than const. ## TODO - [X] adding tests for the feature. - [X] support export const out of module to use. # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect -A clippy::result_large_err` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
This commit is contained in:
66
crates/nu-cmd-lang/src/core_commands/export_const.rs
Normal file
66
crates/nu-cmd-lang/src/core_commands/export_const.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use nu_protocol::ast::Call;
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ExportConst;
|
||||
|
||||
impl Command for ExportConst {
|
||||
fn name(&self) -> &str {
|
||||
"export const"
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Use parse-time constant from a module and export them from this module."
|
||||
}
|
||||
|
||||
fn signature(&self) -> nu_protocol::Signature {
|
||||
Signature::build("export 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::MathExpression)),
|
||||
"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 run(
|
||||
&self,
|
||||
_engine_state: &EngineState,
|
||||
_stack: &mut Stack,
|
||||
_call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
Ok(PipelineData::empty())
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![Example {
|
||||
description: "Re-export a command from another module",
|
||||
example: r#"module spam { export const foo = 3; }
|
||||
module eggs { export use spam foo }
|
||||
use eggs foo
|
||||
foo
|
||||
"#,
|
||||
result: Some(Value::test_int(3)),
|
||||
}]
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["reexport", "import", "module"]
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ mod echo;
|
||||
mod error_make;
|
||||
mod export;
|
||||
mod export_alias;
|
||||
mod export_const;
|
||||
mod export_def;
|
||||
mod export_def_env;
|
||||
mod export_extern;
|
||||
@ -50,6 +51,7 @@ pub use echo::Echo;
|
||||
pub use error_make::ErrorMake;
|
||||
pub use export::ExportCommand;
|
||||
pub use export_alias::ExportAlias;
|
||||
pub use export_const::ExportConst;
|
||||
pub use export_def::ExportDef;
|
||||
pub use export_def_env::ExportDefEnv;
|
||||
pub use export_extern::ExportExtern;
|
||||
|
@ -1,8 +1,8 @@
|
||||
use nu_engine::{eval_block, find_in_dirs_env, get_dirs_var_from_call, redirect_env};
|
||||
use nu_protocol::ast::{Call, Expr, Expression};
|
||||
use nu_protocol::ast::{Call, Expr, Expression, ImportPattern, ImportPatternMember};
|
||||
use nu_protocol::engine::{Command, EngineState, Stack};
|
||||
use nu_protocol::{
|
||||
Category, Example, PipelineData, ShellError, Signature, SyntaxShape, Type, Value,
|
||||
Category, Example, Module, PipelineData, ShellError, Signature, Span, SyntaxShape, Type, Value,
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
@ -110,6 +110,14 @@ This command is a parser keyword. For details, check:
|
||||
// Merge the block's environment to the current stack
|
||||
redirect_env(engine_state, caller_stack, &callee_stack);
|
||||
}
|
||||
|
||||
use_variables(
|
||||
engine_state,
|
||||
import_pattern,
|
||||
module,
|
||||
caller_stack,
|
||||
call.head,
|
||||
);
|
||||
} else {
|
||||
return Err(ShellError::GenericError(
|
||||
format!(
|
||||
@ -162,6 +170,76 @@ This command is a parser keyword. For details, check:
|
||||
}
|
||||
}
|
||||
|
||||
fn use_variables(
|
||||
engine_state: &EngineState,
|
||||
import_pattern: &ImportPattern,
|
||||
module: &Module,
|
||||
caller_stack: &mut Stack,
|
||||
head_span: Span,
|
||||
) {
|
||||
if !module.variables.is_empty() {
|
||||
if import_pattern.members.is_empty() {
|
||||
// add a record variable.
|
||||
if let Some(var_id) = import_pattern.module_name_var_id {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
for (var_name, var_id) in module.variables.iter() {
|
||||
if let Some(val) = engine_state.get_var(*var_id).clone().const_val {
|
||||
cols.push(String::from_utf8_lossy(var_name).to_string());
|
||||
vals.push(val)
|
||||
}
|
||||
}
|
||||
caller_stack.add_var(
|
||||
var_id,
|
||||
Value::record(cols, vals, module.span.unwrap_or(head_span)),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let mut have_glob = false;
|
||||
for m in &import_pattern.members {
|
||||
if matches!(m, ImportPatternMember::Glob { .. }) {
|
||||
have_glob = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if have_glob {
|
||||
// bring all variables into scope directly.
|
||||
for (_, var_id) in module.variables.iter() {
|
||||
if let Some(val) = engine_state.get_var(*var_id).clone().const_val {
|
||||
caller_stack.add_var(*var_id, val);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut members = vec![];
|
||||
for m in &import_pattern.members {
|
||||
match m {
|
||||
ImportPatternMember::List { names, .. } => {
|
||||
for (n, _) in names {
|
||||
if module.variables.contains_key(n) {
|
||||
members.push(n);
|
||||
}
|
||||
}
|
||||
}
|
||||
ImportPatternMember::Name { name, .. } => {
|
||||
if module.variables.contains_key(name) {
|
||||
members.push(name)
|
||||
}
|
||||
}
|
||||
ImportPatternMember::Glob { .. } => continue,
|
||||
}
|
||||
}
|
||||
for m in members {
|
||||
if let Some(var_id) = module.variables.get(m) {
|
||||
if let Some(val) = engine_state.get_var(*var_id).clone().const_val {
|
||||
caller_stack.add_var(*var_id, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
#[test]
|
||||
|
@ -29,6 +29,7 @@ pub fn create_default_context() -> EngineState {
|
||||
ErrorMake,
|
||||
ExportAlias,
|
||||
ExportCommand,
|
||||
ExportConst,
|
||||
ExportDef,
|
||||
ExportDefEnv,
|
||||
ExportExtern,
|
||||
|
Reference in New Issue
Block a user