From 481cae95d181c3ac5d897bb9c2527a1eb3268a9c Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Tue, 3 Feb 2026 02:28:32 +0000 Subject: [PATCH 1/3] enhance expression evaluation logic and add tests for parameter evaluation with expression variables --- .../BuildParamsCommandTests.cs | 93 +++++++++++++++++++ .../Emit/ParameterAssignmentEvaluatorTests.cs | 32 +++++++ .../Emit/ParameterAssignmentEvaluator.cs | 55 ++++++++++- 3 files changed, 175 insertions(+), 5 deletions(-) create mode 100644 src/Bicep.Core.UnitTests/Emit/ParameterAssignmentEvaluatorTests.cs diff --git a/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs b/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs index abf9af83488..b572a4b1365 100644 --- a/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs @@ -327,6 +327,99 @@ param vnetConfigs array paramsObject.Should().HaveValueAtPath("parameters.sharedGroupName.value", "rg-search-foo-nonprod"); } + [TestMethod] + public async Task Build_params_for_expression_variable_should_succeed() + { + var outputPath = FileHelper.GetUniqueTestOutputPath(TestContext); + + _ = FileHelper.SaveResultFile( + TestContext, + "main.bicep", + """ + type FleetConfig = { + namePrefix: string + sku: string + capacity: int + clusteringPolicy: string + } + + param testMatrix FleetConfig[] + """, + outputPath); + + var paramsPath = FileHelper.SaveResultFile( + TestContext, + "main.bicepparam", + """ + using './main.bicep' + + var matrix = [ + { + namePrefix: 'e10impactx4' + sku: 'Enterprise_E10' + capacity: 4 + } + { + namePrefix: 'e10impact' + sku: 'Enterprise_E10' + capacity: 2 + } + ] + + var type1 = [for item in matrix: { + namePrefix: item.namePrefix + sku: item.sku + capacity: item.capacity + clusteringPolicy: 'EnterpriseCluster' + }] + + var type2 = [for item in matrix: { + namePrefix: '${item.namePrefix}-ent' + sku: item.sku + capacity: item.capacity + clusteringPolicy: 'OSSCluster' + }] + + param testMatrix = concat(type1, type2) + """, + outputPath); + + var result = await Bicep(CreateDefaultSettings(), "build-params", paramsPath, "--stdout"); + + result.Should().Succeed(); + + var parametersStdout = result.Stdout.FromJson(); + var paramsObject = parametersStdout.parametersJson.FromJson(); + paramsObject.Should().HaveValueAtPath("parameters.testMatrix.value", JToken.Parse(""" + [ + { + "namePrefix": "e10impactx4", + "sku": "Enterprise_E10", + "capacity": 4, + "clusteringPolicy": "EnterpriseCluster" + }, + { + "namePrefix": "e10impact", + "sku": "Enterprise_E10", + "capacity": 2, + "clusteringPolicy": "EnterpriseCluster" + }, + { + "namePrefix": "e10impactx4-ent", + "sku": "Enterprise_E10", + "capacity": 4, + "clusteringPolicy": "OSSCluster" + }, + { + "namePrefix": "e10impact-ent", + "sku": "Enterprise_E10", + "capacity": 2, + "clusteringPolicy": "OSSCluster" + } + ] + """)); + } + [TestMethod] public async Task Build_params_extends_variable_uses_base_params_not_overridden() { diff --git a/src/Bicep.Core.UnitTests/Emit/ParameterAssignmentEvaluatorTests.cs b/src/Bicep.Core.UnitTests/Emit/ParameterAssignmentEvaluatorTests.cs new file mode 100644 index 00000000000..c7e1e7130ce --- /dev/null +++ b/src/Bicep.Core.UnitTests/Emit/ParameterAssignmentEvaluatorTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace Bicep.Core.UnitTests.Emit; + +[TestClass] +public class ParameterAssignmentEvaluatorTests +{ + [TestMethod] + public void BuildParams_ForExpressionVariable_EvaluatesToValue() + { + var services = new ServiceBuilder().WithEmptyAzResources(); + + var result = CompilationHelper.CompileParams( + services, + ("main.bicep", "param p int[]"), + ("parameters.bicepparam", """ + using 'main.bicep' + + var x = [for item in [1, 2]: item * 2] + param p = x + """)); + + result.Should().NotHaveAnyDiagnostics(); + result.Parameters.Should().HaveValueAtPath("parameters.p.value", JToken.Parse("[2, 4]")); + } +} diff --git a/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs b/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs index ff4b246d06d..7eb561a57c6 100644 --- a/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs +++ b/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs @@ -25,6 +25,24 @@ namespace Bicep.Core.Emit; public class ParameterAssignmentEvaluator { + private sealed class ForLoopIndexRewriter : ExpressionRewriteVisitor + { + private readonly long index; + + private ForLoopIndexRewriter(long index) + { + this.index = index; + } + + public static Expression Rewrite(Expression expression, long index) + { + return new ForLoopIndexRewriter(index).Replace(expression); + } + + public override Expression ReplaceCopyIndexExpression(CopyIndexExpression expression) => new IntegerLiteralExpression(expression.SourceSyntax, index); + + public override Expression ReplaceForLoopExpression(ForLoopExpression expression) => expression; + } private class ParameterAssignmentEvaluationContext : IEvaluationContext { private readonly TemplateExpressionEvaluationHelper evaluationHelper; @@ -419,7 +437,7 @@ public Result EvaluateParameter(ParameterAssignmentSymbol parameter) try { - return Result.For(parameterConverter.ConvertExpression(intermediate).EvaluateExpression(context)); + return Result.For(EvaluateExpression(parameterConverter, intermediate, context)); } catch (Exception ex) { @@ -478,7 +496,7 @@ public ImmutableDictionary EvaluateExtensionConfigAssignment(Ext { try { - propertyResult = Result.For(converter.ConvertExpression(intermediate).EvaluateExpression(context)); + propertyResult = Result.For(EvaluateExpression(converter, intermediate, context)); } catch (Exception ex) { @@ -542,7 +560,7 @@ private Result EvaluateVariable(VariableSymbol variable) var variableConverter = GetConverterForVariable(variable); var intermediate = variableConverter.ConvertToIntermediateExpression(variable.DeclaringVariable.Value); - return Result.For(variableConverter.ConvertExpression(intermediate).EvaluateExpression(context)); + return Result.For(EvaluateExpression(variableConverter, intermediate, context)); } catch (Exception ex) { @@ -560,7 +578,7 @@ private Result EvaluateSynthesizeVariableExpression(string name, Expression expr { var evalContext = GetExpressionEvaluationContextForModel(model); var exprConverter = converterCache.GetOrAdd(model, m => new ExpressionConverter(new EmitterContext(m))); - return Result.For(exprConverter.ConvertExpression(expression).EvaluateExpression(evalContext)); + return Result.For(EvaluateExpression(exprConverter, expression, evalContext)); } catch (Exception e) { @@ -790,7 +808,7 @@ public ExpressionEvaluationResult EvaluateExpression(SyntaxBase expressionSyntax { var context = GetExpressionEvaluationContext(); var intermediate = converter.ConvertToIntermediateExpression(expressionSyntax); - var result = converter.ConvertExpression(intermediate).EvaluateExpression(context); + var result = EvaluateExpression(converter, intermediate, context); return ExpressionEvaluationResult.For(result); } catch (Exception ex) @@ -800,6 +818,33 @@ public ExpressionEvaluationResult EvaluateExpression(SyntaxBase expressionSyntax } } + private JToken EvaluateExpression(ExpressionConverter expressionConverter, Expression expression, ParameterAssignmentEvaluationContext context) + { + if (expression is ForLoopExpression forLoop) + { + return EvaluateForLoopExpression(expressionConverter, forLoop, context); + } + + return expressionConverter.ConvertExpression(expression).EvaluateExpression(context); + } + + private JToken EvaluateForLoopExpression(ExpressionConverter expressionConverter, ForLoopExpression forLoop, ParameterAssignmentEvaluationContext context) + { + var source = EvaluateExpression(expressionConverter, forLoop.Expression, context); + if (source is not JArray sourceArray) + { + throw new InvalidOperationException("For-expression source must be an array."); + } + + var results = new JArray(); + for (var i = 0; i < sourceArray.Count; i++) + { + results.Add(EvaluateExpression(expressionConverter, ForLoopIndexRewriter.Rewrite(forLoop.Body, i), context)); + } + + return results; + } + /// /// Rewrites the external input function calls to use the externalInputs function with the index of the external input. /// e.g. externalInput('sys.cli', 'foo') becomes externalInputs('0') From 87cf2222f55e7b6cd378e49fbf8085cec9da03b2 Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:59:05 +0000 Subject: [PATCH 2/3] Fix external input reference handling in ReplaceFunctionCallExpression --- src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs b/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs index 4d49353dc36..09f367f2458 100644 --- a/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs +++ b/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs @@ -862,12 +862,13 @@ public static Expression Rewrite( public override Expression ReplaceFunctionCallExpression(FunctionCallExpression expression) { if (expression.SourceSyntax is FunctionCallSyntaxBase functionCallSyntax && - externalInputReferences.ExternalInputInfoBySyntax.TryGetValue(functionCallSyntax, out var info)) + externalInputReferences.InfoBySyntax.TryGetValue(functionCallSyntax, out var info) && + info.Length > 0) { return new FunctionCallExpression( functionCallSyntax, LanguageConstants.ExternalInputsArmFunctionName, - [ExpressionFactory.CreateStringLiteral(info.DefinitionKey)] + [ExpressionFactory.CreateStringLiteral(info[0].DefinitionKey)] ); } From e93b72a16f88a75c78cf4e9e8aba1367553b6f2d Mon Sep 17 00:00:00 2001 From: Engin Polat <118744+polatengin@users.noreply.github.com> Date: Sat, 28 Mar 2026 02:34:10 +0000 Subject: [PATCH 3/3] format document --- .../Emit/ParameterAssignmentEvaluator.cs | 107 +++++++++--------- 1 file changed, 53 insertions(+), 54 deletions(-) diff --git a/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs b/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs index a48687b7f10..20d34edecf0 100644 --- a/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs +++ b/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs @@ -888,71 +888,70 @@ public override Expression ReplaceFunctionCallExpression(FunctionCallExpression return base.ReplaceFunctionCallExpression(expression); } - } - // TODO: Should look into using ITemplateLanguageExpression for ParameterAssignmentEvaluator - // which would probably simplify a lot of this logic. - /// - /// Rewrites intermediate expressions by replacing variable references, parameter assignment references, - /// and user-defined function calls with their evaluated literal values. This is used for parameter - /// assignments that must be inlined (e.g., those containing externalInput calls), where the - /// emitted JSON parameter file cannot reference ARM template variables, parameters, or user-defined - /// functions. - /// - public class InlinedParameterRewriter( - SemanticModel model, - ParameterAssignmentEvaluator evaluator, - ExpressionConverter converter) : ExpressionRewriteVisitor - { - public Expression Rewrite(Expression expression) => Replace(expression); + // TODO: Should look into using ITemplateLanguageExpression for ParameterAssignmentEvaluator + // which would probably simplify a lot of this logic. + /// + /// Rewrites intermediate expressions by replacing variable references, parameter assignment references, + /// and user-defined function calls with their evaluated literal values. This is used for parameter + /// assignments that must be inlined (e.g., those containing externalInput calls), where the + /// emitted JSON parameter file cannot reference ARM template variables, parameters, or user-defined + /// functions. + /// + public class InlinedParameterRewriter( + SemanticModel model, + ParameterAssignmentEvaluator evaluator, + ExpressionConverter converter) : ExpressionRewriteVisitor + { + public Expression Rewrite(Expression expression) => Replace(expression); - public override Expression ReplaceVariableReferenceExpression(VariableReferenceExpression expression) - => ResultToExpression(evaluator.EvaluateVariable(expression.Variable), expression); + public override Expression ReplaceVariableReferenceExpression(VariableReferenceExpression expression) + => ResultToExpression(evaluator.EvaluateVariable(expression.Variable), expression); - public override Expression ReplaceParametersAssignmentReferenceExpression(ParametersAssignmentReferenceExpression expression) - => ResultToExpression(evaluator.EvaluateParameter(expression.Parameter), expression); + public override Expression ReplaceParametersAssignmentReferenceExpression(ParametersAssignmentReferenceExpression expression) + => ResultToExpression(evaluator.EvaluateParameter(expression.Parameter), expression); - public override Expression ReplaceImportedVariableReferenceExpression(ImportedVariableReferenceExpression expression) - => ResultToExpression(evaluator.EvaluateImport(expression.Variable), expression); + public override Expression ReplaceImportedVariableReferenceExpression(ImportedVariableReferenceExpression expression) + => ResultToExpression(evaluator.EvaluateImport(expression.Variable), expression); - public override Expression ReplaceWildcardImportVariablePropertyReferenceExpression(WildcardImportVariablePropertyReferenceExpression expression) - => ResultToExpression(evaluator.EvaluateWildcardImportPropertyAsVariable(new WildcardImportPropertyReference(expression.ImportSymbol, expression.PropertyName)), expression); + public override Expression ReplaceWildcardImportVariablePropertyReferenceExpression(WildcardImportVariablePropertyReferenceExpression expression) + => ResultToExpression(evaluator.EvaluateWildcardImportPropertyAsVariable(new WildcardImportPropertyReference(expression.ImportSymbol, expression.PropertyName)), expression); - public override Expression ReplaceUserDefinedFunctionCallExpression(UserDefinedFunctionCallExpression expression) - => EvaluateToJToken(base.ReplaceUserDefinedFunctionCallExpression(expression)); + public override Expression ReplaceUserDefinedFunctionCallExpression(UserDefinedFunctionCallExpression expression) + => EvaluateToJToken(base.ReplaceUserDefinedFunctionCallExpression(expression)); - public override Expression ReplaceImportedUserDefinedFunctionCallExpression(ImportedUserDefinedFunctionCallExpression expression) - => EvaluateToJToken(base.ReplaceImportedUserDefinedFunctionCallExpression(expression)); + public override Expression ReplaceImportedUserDefinedFunctionCallExpression(ImportedUserDefinedFunctionCallExpression expression) + => EvaluateToJToken(base.ReplaceImportedUserDefinedFunctionCallExpression(expression)); - public override Expression ReplaceWildcardImportInstanceFunctionCallExpression(WildcardImportInstanceFunctionCallExpression expression) - => EvaluateToJToken(base.ReplaceWildcardImportInstanceFunctionCallExpression(expression)); + public override Expression ReplaceWildcardImportInstanceFunctionCallExpression(WildcardImportInstanceFunctionCallExpression expression) + => EvaluateToJToken(base.ReplaceWildcardImportInstanceFunctionCallExpression(expression)); - private static Expression ResultToExpression(Result result, Expression expression) - => result.Value is { } value - ? JTokenToExpression(value, expression.SourceSyntax) - : throw new ExpressionException(result.Diagnostic?.Message ?? "Failed to evaluate expression."); + private static Expression ResultToExpression(Result result, Expression expression) + => result.Value is { } value + ? JTokenToExpression(value, expression.SourceSyntax) + : throw new ExpressionException(result.Diagnostic?.Message ?? "Failed to evaluate expression."); - private Expression EvaluateToJToken(Expression expression) - { - var context = evaluator.GetExpressionEvaluationContextForModel(model); - return JTokenToExpression(converter.ConvertExpression(expression).EvaluateExpression(context), expression.SourceSyntax); - } + private Expression EvaluateToJToken(Expression expression) + { + var context = evaluator.GetExpressionEvaluationContextForModel(model); + return JTokenToExpression(converter.ConvertExpression(expression).EvaluateExpression(context), expression.SourceSyntax); + } - private static Expression JTokenToExpression(JToken token, SyntaxBase? syntax) => token switch + private static Expression JTokenToExpression(JToken token, SyntaxBase? syntax) => token switch + { + JObject obj => ExpressionFactory.CreateObject(obj.Properties().Select(p => ExpressionFactory.CreateObjectProperty(p.Name, JTokenToExpression(p.Value, syntax), syntax)), syntax), + JArray arr => ExpressionFactory.CreateArray(arr.Select(item => JTokenToExpression(item, syntax)), syntax), + JValue jValue => jValue.Value switch { - JObject obj => ExpressionFactory.CreateObject(obj.Properties().Select(p => ExpressionFactory.CreateObjectProperty(p.Name, JTokenToExpression(p.Value, syntax), syntax)), syntax), - JArray arr => ExpressionFactory.CreateArray(arr.Select(item => JTokenToExpression(item, syntax)), syntax), - JValue jValue => jValue.Value switch - { - string @string => ExpressionFactory.CreateStringLiteral(@string, syntax), - int @int => ExpressionFactory.CreateIntegerLiteral(@int, syntax), - long @long => ExpressionFactory.CreateIntegerLiteral(@long, syntax), - bool @bool => ExpressionFactory.CreateBooleanLiteral(@bool, syntax), - null => new NullLiteralExpression(syntax), - _ => throw new ExpressionException($"Unrecognized JValue value of type {jValue.Value?.GetType().Name}"), - }, - _ => throw new ExpressionException($"Unsupported JToken type: {token.Type}"), - }; - } + string @string => ExpressionFactory.CreateStringLiteral(@string, syntax), + int @int => ExpressionFactory.CreateIntegerLiteral(@int, syntax), + long @long => ExpressionFactory.CreateIntegerLiteral(@long, syntax), + bool @bool => ExpressionFactory.CreateBooleanLiteral(@bool, syntax), + null => new NullLiteralExpression(syntax), + _ => throw new ExpressionException($"Unrecognized JValue value of type {jValue.Value?.GetType().Name}"), + }, + _ => throw new ExpressionException($"Unsupported JToken type: {token.Type}"), + }; + } }