-
Notifications
You must be signed in to change notification settings - Fork 54
Add lambda expression and map() function with ARM syntax
#1238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| Describe 'map() function with lambda tests' { | ||
| It 'map with simple lambda multiplies each element by 2' { | ||
| $config_yaml = @' | ||
| $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json | ||
| parameters: | ||
| numbers: | ||
| type: array | ||
| defaultValue: [1, 2, 3] | ||
| resources: | ||
| - name: Echo | ||
| type: Microsoft.DSC.Debug/Echo | ||
| properties: | ||
| output: "[map(parameters('numbers'), lambda('x', mul(lambdaVariables('x'), 2)))]" | ||
| '@ | ||
| $out = $config_yaml | dsc config get -f - | ConvertFrom-Json | ||
| $LASTEXITCODE | Should -Be 0 | ||
| $out.results[0].result.actualState.output | Should -Be @(2,4,6) | ||
| } | ||
|
|
||
| It 'map with lambda using index parameter' { | ||
| $config_yaml = @' | ||
| $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json | ||
| parameters: | ||
| items: | ||
| type: array | ||
| defaultValue: [10, 20, 30] | ||
| resources: | ||
| - name: Echo | ||
| type: Microsoft.DSC.Debug/Echo | ||
| properties: | ||
| output: "[map(parameters('items'), lambda('val', 'i', add(lambdaVariables('val'), lambdaVariables('i'))))]" | ||
| '@ | ||
| $out = $config_yaml | dsc config get -f - | ConvertFrom-Json | ||
| $LASTEXITCODE | Should -Be 0 | ||
| $out.results[0].result.actualState.output | Should -Be @(10,21,32) | ||
| } | ||
|
|
||
| It 'map with range generates array' { | ||
| $config_yaml = @' | ||
| $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json | ||
| resources: | ||
| - name: Echo | ||
| type: Microsoft.DSC.Debug/Echo | ||
| properties: | ||
| output: "[map(range(0, 3), lambda('x', mul(lambdaVariables('x'), 3)))]" | ||
| '@ | ||
| $out = $config_yaml | dsc config get -f - | ConvertFrom-Json | ||
| $LASTEXITCODE | Should -Be 0 | ||
| $out.results[0].result.actualState.output | Should -Be @(0,3,6) | ||
| } | ||
|
|
||
| It 'map returns empty array for empty input' { | ||
| $config_yaml = @' | ||
| $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json | ||
| resources: | ||
| - name: Echo | ||
| type: Microsoft.DSC.Debug/Echo | ||
| properties: | ||
| output: "[map(createArray(), lambda('x', mul(lambdaVariables('x'), 2)))]" | ||
| '@ | ||
| $out = $config_yaml | dsc config get -f - | ConvertFrom-Json | ||
| $LASTEXITCODE | Should -Be 0 | ||
| $out.results[0].result.actualState.output | Should -Be $null | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,6 +25,8 @@ pub struct Context { | |||||||||||||
| pub dsc_version: Option<String>, | ||||||||||||||
| pub execution_type: ExecutionKind, | ||||||||||||||
| pub extensions: Vec<DscExtension>, | ||||||||||||||
| pub lambda_variables: HashMap<String, Value>, | ||||||||||||||
|
Comment on lines
27
to
+28
|
||||||||||||||
| pub extensions: Vec<DscExtension>, | |
| pub lambda_variables: HashMap<String, Value>, | |
| pub extensions: Vec<DscExtension>, | |
| /// Storage for lambda parameter bindings during lambda body evaluation | |
| pub lambda_variables: HashMap<String, Value>, | |
| /// Registry of lambda functions by their unique ID |
Copilot
AI
Nov 12, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using RefCell<HashMap> for lambdas in a Clone-able Context has implications for how lambda storage works across context clones. When context.clone() is called (e.g., in map.rs line 64), the RefCell is cloned which creates a new RefCell with a new HashMap that is a copy of the original. This means:
- Lambdas created in the cloned context won't be visible in the original context
- The original lambdas are duplicated, which may not be the intended behavior
If lambdas should be shared across context clones, use Rc<RefCell<HashMap<String, Lambda>>> instead. If lambdas should be scoped to each context clone (current behavior), document this design decision with a comment.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use crate::DscError; | ||
| use crate::configure::context::Context; | ||
| use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata}; | ||
| use rust_i18n::t; | ||
| use serde_json::Value; | ||
|
|
||
|
|
||
| /// The lambda() function is special - it's not meant to be invoked directly | ||
| /// through the normal function dispatcher path. Instead, it's caught in the | ||
| /// Function::invoke method and handled specially via invoke_lambda(). | ||
| /// | ||
| /// This struct exists for metadata purposes and to signal errors if someone | ||
| /// tries to invoke lambda() as a regular function (which shouldn't happen). | ||
| #[derive(Debug, Default)] | ||
| pub struct LambdaFn {} | ||
|
|
||
| impl Function for LambdaFn { | ||
| fn get_metadata(&self) -> FunctionMetadata { | ||
| FunctionMetadata { | ||
| name: "lambda".to_string(), | ||
| description: t!("functions.lambda.description").to_string(), | ||
| category: vec![FunctionCategory::Lambda], | ||
| min_args: 2, | ||
| max_args: 10, // Up to 9 parameters + 1 body | ||
| accepted_arg_ordered_types: vec![], | ||
| remaining_arg_accepted_types: None, | ||
| return_types: vec![FunctionArgKind::Object], // Lambda is represented as a special object | ||
| } | ||
| } | ||
|
|
||
| fn invoke(&self, _args: &[Value], _context: &Context) -> Result<Value, DscError> { | ||
| // This should never be called - lambda() is handled specially in Function::invoke | ||
| Err(DscError::Parser(t!("functions.lambda.cannotInvokeDirectly").to_string())) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use crate::DscError; | ||
| use crate::configure::context::Context; | ||
| use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata}; | ||
| use rust_i18n::t; | ||
| use serde_json::Value; | ||
| use tracing::debug; | ||
|
|
||
| #[derive(Debug, Default)] | ||
| pub struct LambdaVariables {} | ||
|
|
||
| impl Function for LambdaVariables { | ||
| fn get_metadata(&self) -> FunctionMetadata { | ||
| FunctionMetadata { | ||
| name: "lambdaVariables".to_string(), | ||
| description: t!("functions.lambdaVariables.description").to_string(), | ||
| category: vec![FunctionCategory::Lambda], | ||
| min_args: 1, | ||
| max_args: 1, | ||
| accepted_arg_ordered_types: vec![vec![FunctionArgKind::String]], | ||
| remaining_arg_accepted_types: None, | ||
| return_types: vec![ | ||
| FunctionArgKind::String, | ||
| FunctionArgKind::Number, | ||
| FunctionArgKind::Boolean, | ||
| FunctionArgKind::Array, | ||
| FunctionArgKind::Object, | ||
| FunctionArgKind::Null, | ||
| ], | ||
| } | ||
| } | ||
|
|
||
| fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> { | ||
| debug!("{}", t!("functions.lambdaVariables.invoked")); | ||
|
|
||
| if args.len() != 1 { | ||
| return Err(DscError::Parser(t!("functions.invalidArgCount", name = "lambdaVariables", count = 1).to_string())); | ||
| } | ||
|
|
||
| let Some(var_name) = args[0].as_str() else { | ||
| return Err(DscError::Parser(t!("functions.lambdaVariables.paramNameMustBeString").to_string())); | ||
| }; | ||
|
|
||
| // Look up the variable in the lambda context | ||
| if let Some(value) = context.lambda_variables.get(var_name) { | ||
| Ok(value.clone()) | ||
| } else { | ||
| Err(DscError::Parser(t!("functions.lambdaVariables.notFound", name = var_name).to_string())) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use serde_json::json; | ||
|
|
||
| #[test] | ||
| fn lookup_existing_variable() { | ||
| let mut context = Context::new(); | ||
| context.lambda_variables.insert("x".to_string(), json!(42)); | ||
|
|
||
| let func = LambdaVariables {}; | ||
| let result = func.invoke(&[Value::String("x".to_string())], &context).unwrap(); | ||
| assert_eq!(result, json!(42)); | ||
| } | ||
|
|
||
| #[test] | ||
| fn lookup_nonexistent_variable() { | ||
| let context = Context::new(); | ||
| let func = LambdaVariables {}; | ||
| let result = func.invoke(&[Value::String("x".to_string())], &context); | ||
| assert!(result.is_err()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| use crate::DscError; | ||
| use crate::configure::context::Context; | ||
| use crate::functions::{FunctionArgKind, Function, FunctionCategory, FunctionMetadata, FunctionDispatcher}; | ||
| use rust_i18n::t; | ||
| use serde_json::Value; | ||
| use tracing::debug; | ||
|
|
||
| #[derive(Debug, Default)] | ||
| pub struct Map {} | ||
|
|
||
| impl Function for Map { | ||
| fn get_metadata(&self) -> FunctionMetadata { | ||
| FunctionMetadata { | ||
| name: "map".to_string(), | ||
| description: t!("functions.map.description").to_string(), | ||
| category: vec![FunctionCategory::Array, FunctionCategory::Lambda], | ||
| min_args: 2, | ||
| max_args: 2, | ||
| accepted_arg_ordered_types: vec![ | ||
| vec![FunctionArgKind::Array], | ||
| vec![FunctionArgKind::String], // Lambda ID as string | ||
| ], | ||
| remaining_arg_accepted_types: None, | ||
| return_types: vec![FunctionArgKind::Array], | ||
| } | ||
| } | ||
|
|
||
| fn invoke(&self, args: &[Value], context: &Context) -> Result<Value, DscError> { | ||
| debug!("{}", t!("functions.map.invoked")); | ||
|
|
||
| if args.len() != 2 { | ||
| return Err(DscError::Parser(t!("functions.invalidArgCount", name = "map", count = 2).to_string())); | ||
| } | ||
|
|
||
| let Some(array) = args[0].as_array() else { | ||
| return Err(DscError::Parser(t!("functions.map.firstArgMustBeArray").to_string())); | ||
| }; | ||
|
|
||
| let Some(lambda_id) = args[1].as_str() else { | ||
| return Err(DscError::Parser(t!("functions.map.secondArgMustBeLambda").to_string())); | ||
| }; | ||
|
|
||
| // Retrieve the lambda from context | ||
| let lambdas = context.lambdas.borrow(); | ||
| let Some(lambda) = lambdas.get(lambda_id) else { | ||
| return Err(DscError::Parser(t!("functions.map.lambdaNotFound", id = lambda_id).to_string())); | ||
| }; | ||
|
|
||
| // Validate parameter count (1 or 2 parameters) | ||
| if lambda.parameters.is_empty() || lambda.parameters.len() > 2 { | ||
| return Err(DscError::Parser(t!("functions.map.lambdaMustHave1Or2Params").to_string())); | ||
| } | ||
|
|
||
| // Create function dispatcher for evaluating lambda body | ||
| let dispatcher = FunctionDispatcher::new(); | ||
| let mut result_array = Vec::new(); | ||
|
|
||
| // Iterate through array and evaluate lambda for each element | ||
| for (index, element) in array.iter().enumerate() { | ||
| // Create a new context with lambda variables bound | ||
| let mut lambda_context = context.clone(); | ||
|
Comment on lines
+47
to
+64
|
||
|
|
||
| // Bind first parameter to array element | ||
| lambda_context.lambda_variables.insert( | ||
| lambda.parameters[0].clone(), | ||
| element.clone() | ||
| ); | ||
|
|
||
| // Bind second parameter to index if provided | ||
| if lambda.parameters.len() == 2 { | ||
| lambda_context.lambda_variables.insert( | ||
| lambda.parameters[1].clone(), | ||
| Value::Number(serde_json::Number::from(index)) | ||
| ); | ||
| } | ||
|
|
||
| // Evaluate lambda body with bound variables | ||
| let result = lambda.body.invoke(&dispatcher, &lambda_context)?; | ||
| result_array.push(result); | ||
| } | ||
|
|
||
| Ok(Value::Array(result_array)) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
|
|
||
| #[test] | ||
| fn requires_two_args() { | ||
| let func = Map {}; | ||
| let result = func.invoke(&[], &Context::new()); | ||
| assert!(result.is_err()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test expects
$nullfor an empty array output, but this may not be the correct assertion. An empty array@()is different from$nullin PowerShell. Verify that DSC actually returns$nullfor empty arrays, or update the assertion to check for an empty array:$out.results[0].result.actualState.output.Count | Should -Be 0.