Skip to content

Conversation

@Gijsreyn
Copy link
Contributor

@Gijsreyn Gijsreyn commented Nov 5, 2025

PR Summary

This PR implements lambda expressions and the map() function for DSC configuration documents, enabling array transformations. The implementation uses ARM compatible syntax e.g., (lambda('param', body) and lambdaVariables('param'))

Example usage:

# Transform array elements
map(createArray(1, 2, 3), lambda('x', mul(lambdaVariables('x'), 2)))
# Returns: [2, 4, 6]

Seperated tests in a different file.

PR Context

Partially addresses #57.

@Gijsreyn Gijsreyn marked this pull request as ready for review November 5, 2025 03:30
@SteveL-MSFT SteveL-MSFT requested a review from Copilot November 12, 2025 22:33
Copilot finished reviewing on behalf of SteveL-MSFT November 12, 2025 22:37
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements lambda expressions and the map() function for DSC configuration documents, using ARM-compatible syntax (lambda('param', body) and lambdaVariables('param')). The implementation stores lambdas in the context with unique UUID-based identifiers and evaluates them by binding lambda parameters to array elements.

Key changes:

  • Added Lambda struct and lambda-specific handling in the function evaluation pipeline
  • Implemented map(), lambda(), and lambdaVariables() functions for array transformations
  • Extended Context with lambda storage (lambdas) and lambda parameter bindings (lambda_variables)

Reviewed Changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
lib/dsc-lib/src/parser/functions.rs Added Lambda struct and FunctionArg::Lambda variant; special handling for lambda() function to bypass normal evaluation
lib/dsc-lib/src/functions/mod.rs Implemented invoke_lambda() to create and store lambda objects with UUID-based IDs; registered new lambda-related functions
lib/dsc-lib/src/functions/map.rs Implemented array transformation function that evaluates lambdas for each element with optional index parameter
lib/dsc-lib/src/functions/lambda_variables.rs Implemented parameter lookup function for accessing lambda-bound variables during evaluation
lib/dsc-lib/src/functions/lambda.rs Created lambda function stub that returns error if invoked directly (handled specially elsewhere)
lib/dsc-lib/src/configure/context.rs Added lambda_variables HashMap and lambdas RefCell for lambda storage and parameter binding
lib/dsc-lib/locales/en-us.toml Added localized error messages and descriptions for lambda-related functions
dsc/tests/dsc_lambda.tests.ps1 Added integration tests for map() with various lambda scenarios (multiplication, index usage, range, empty array)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 27 to +28
pub extensions: Vec<DscExtension>,
pub lambda_variables: HashMap<String, Value>,
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new fields lambda_variables and lambdas lack documentation comments explaining their purpose and usage. Add doc comments to clarify:

  • lambda_variables: Storage for lambda parameter bindings during lambda body evaluation
  • lambdas: Registry of lambda functions by their unique ID
Suggested change
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 uses AI. Check for mistakes.
pub execution_type: ExecutionKind,
pub extensions: Vec<DscExtension>,
pub lambda_variables: HashMap<String, Value>,
pub lambdas: std::cell::RefCell<HashMap<String, crate::parser::functions::Lambda>>,
Copy link

Copilot AI Nov 12, 2025

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:

  1. Lambdas created in the cloned context won't be visible in the original context
  2. 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.

Copilot uses AI. Check for mistakes.
Lambda(Lambda),
}

#[derive(Clone)]
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Lambda struct should derive Debug for consistency with other types in the codebase and to aid debugging. Since Expression is Clone, Lambda can also derive Debug without issues.

Suggested change
#[derive(Clone)]
#[derive(Clone, Debug)]

Copilot uses AI. Check for mistakes.
Comment on lines +26 to 34
Lambda(Lambda),
}

#[derive(Clone)]
pub struct Lambda {
pub parameters: Vec<String>,
pub body: Expression,
}

Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FunctionArg::Lambda variant is never constructed - it's only matched to return an error. This suggests it's dead code. Consider removing this variant since lambda functions are handled specially via the string ID mechanism, not as a direct FunctionArg type.

Suggested change
Lambda(Lambda),
}
#[derive(Clone)]
pub struct Lambda {
pub parameters: Vec<String>,
pub body: Expression,
}
}

Copilot uses AI. Check for mistakes.
// Return the ID as a string value
Ok(Value::String(lambda_id))
}

Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lambdas created during expression evaluation are stored in context.lambdas but are never cleaned up or removed. This can lead to a memory leak in configurations that create many lambda expressions (e.g., in loops or repeated evaluations). Consider implementing a cleanup mechanism or using a different scoping strategy for lambda lifetime management.

Suggested change
/// Removes a lambda from the context by its ID.
/// Call this after you are done using a lambda to avoid memory leaks.
pub fn remove_lambda(context: &Context, lambda_id: &str) {
context.lambdas.borrow_mut().remove(lambda_id);
}

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +64
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();
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The lambda is borrowed from context.lambdas and then cloned on line 64, but the borrow from line 47 is not explicitly dropped before the clone. While Rust's borrow checker allows this because the borrow is only used to get the lambda reference and doesn't extend past line 55, it would be clearer to explicitly drop the borrow before cloning the context. Consider restructuring as:

let lambda = {
    let lambdas = context.lambdas.borrow();
    lambdas.get(lambda_id)
        .ok_or_else(|| DscError::Parser(t!("functions.map.lambdaNotFound", id = lambda_id).to_string()))?
        .clone()
};
// ... validate parameter count ...
// Then use lambda directly without holding the borrow

This makes it clearer that we're cloning the lambda value and releasing the borrow before cloning the context.

Copilot uses AI. Check for mistakes.
'@
$out = $config_yaml | dsc config get -f - | ConvertFrom-Json
$LASTEXITCODE | Should -Be 0
$out.results[0].result.actualState.output | Should -Be $null
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test expects $null for an empty array output, but this may not be the correct assertion. An empty array @() is different from $null in PowerShell. Verify that DSC actually returns $null for empty arrays, or update the assertion to check for an empty array: $out.results[0].result.actualState.output.Count | Should -Be 0.

Suggested change
$out.results[0].result.actualState.output | Should -Be $null
$out.results[0].result.actualState.output.Count | Should -Be 0

Copilot uses AI. Check for mistakes.
Lambda(Lambda),
}

#[derive(Clone)]
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing documentation for the Lambda struct. Since this is a public struct (indicated by pub), it should have a doc comment explaining its purpose, fields, and how it's used in the lambda expression system.

Suggested change
#[derive(Clone)]
#[derive(Clone)]
/// Represents a lambda expression in the lambda expression system.
///
/// The `Lambda` struct encapsulates a set of parameter names and an expression body.
/// It is used to model anonymous functions or closures that can be passed as arguments
/// or evaluated within the expression system.
///
/// # Fields
/// - `parameters`: The names of the parameters that the lambda accepts.
/// - `body`: The expression that forms the body of the lambda.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant