From b137c316fc39b7fcabfb0664f69b3a5dc1f88e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Mon, 5 Oct 2026 09:16:43 +0200 Subject: [PATCH 1/2] Support command options in dynamic exec calls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 + .../Parser/CommandExecutionTests.cs | 137 ++++++++++++++++++ .../Parser/StatementParserEdgeTests.cs | 37 +++++ .../CommandArgumentFormatter.cs | 9 +- .../Statement/CommandOption.cs | 18 ++- .../Statement/ExecStatement.cs | 7 +- .../StatementParser.cs | 136 ++++++++--------- README.md | 2 + docs/commands.md | 3 + docs/programming.md | 2 + 10 files changed, 272 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 760da1d1..79d87b95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Parse `exec` options and shell words like direct commands. Bind built-in options normally and pass option-shaped words to functions and script files as positional text. + ## 1.1.271-preview — 2026-10-02 ### New features diff --git a/CosmosDBShell.Tests/Parser/CommandExecutionTests.cs b/CosmosDBShell.Tests/Parser/CommandExecutionTests.cs index 3c6a87f8..8c7a0c7e 100644 --- a/CosmosDBShell.Tests/Parser/CommandExecutionTests.cs +++ b/CosmosDBShell.Tests/Parser/CommandExecutionTests.cs @@ -8,6 +8,7 @@ namespace CosmosShell.Tests.Parser; using System.Threading; using System.Threading.Tasks; +using Azure.Data.Cosmos.Shell.Commands; using Azure.Data.Cosmos.Shell.Core; using Azure.Data.Cosmos.Shell.Parser; @@ -92,6 +93,123 @@ public async Task Exec_VariableCommand_RunsResolvedCommand() Assert.Equal("hello world", Assert.IsType(state.Result).Text); } + [Theory] + [InlineData("--text value --flag --max 5", "value", true)] + [InlineData("-t value -f -m 5", "value", true)] + [InlineData("--text=value --flag=true --max=5", "value", true)] + [InlineData("--text:value --flag:false --max:5", "value", false)] + [InlineData("--text=\"two words\" --flag --max=(2 + 3)", "two words", true)] + [InlineData("--text=$text --flag --max=$max", "two words", true)] + public async Task Exec_BindsOptionsLikeDirectCommands(string arguments, string expectedText, bool expectedFlag) + { + Assert.True(CommandFactory.TryCreateFactory(typeof(ExecBindingCommand), out var factory)); + Shell.App.Commands["execbinding"] = factory; + SetVariable("cmd", new ShellText("execbinding")); + SetVariable("text", new ShellText("two words")); + SetVariable("max", new ShellNumber(5)); + try + { + var direct = await Shell.RunCommandAsync(new(), $"execbinding {arguments}", CancellationToken.None); + var dynamic = await Shell.RunCommandAsync(new(), $"exec $cmd {arguments}", CancellationToken.None); + + Assert.False(direct.IsError); + Assert.False(dynamic.IsError); + var value = Assert.IsType(dynamic.Result).Value; + Assert.Equal(Assert.IsType(direct.Result).Value.GetRawText(), value.GetRawText()); + Assert.Equal(expectedText, value.GetProperty("text").GetString()); + Assert.Equal(expectedFlag, value.GetProperty("flag").GetBoolean()); + Assert.Equal(5, value.GetProperty("max").GetInt32()); + } + finally + { + Shell.App.Commands.Remove("execbinding"); + } + } + + [Fact] + public async Task Exec_HelpOption_ShowsHelp() + { + var state = await Shell.RunCommandAsync(new(), "exec \"echo\" --help", CancellationToken.None); + Assert.False(state.IsError); + } + + [Theory] + [InlineData("--bogus=1", typeof(UnknownOptionException))] + [InlineData("--db", typeof(CommandException))] + public async Task Exec_InvalidOptions_ReportCommandErrors(string arguments, Type exceptionType) + { + var exception = await Assert.ThrowsAnyAsync(() => + Shell.RunCommandAsync(new(), $"exec \"info\" {arguments}", CancellationToken.None)); + Assert.IsType(exceptionType, exception); + } + + [Theory] + [InlineData("exec \"identity\" --name=$text", "--name=two words")] + [InlineData("identity --name:$text", "--name:two words")] + [InlineData("$result = (identity --name=$text)", "--name=two words")] + [InlineData("exec \"identity\" --help", "--help")] + [InlineData("exec \"identity\" -m", "-m")] + public async Task Function_OptionWords_ArePositionalText(string invocation, string expected) + { + SetVariable("text", new ShellText("two words")); + var state = await Shell.RunCommandAsync( + new(), $"def identity [value] {{ return $value }}; {invocation}", CancellationToken.None); + + Assert.False(state.IsError); + var result = invocation.StartsWith("$result", StringComparison.Ordinal) ? GetVariable("result") : state.Result; + Assert.Equal(expected, Assert.IsType(result).Text); + } + + [Theory] + [InlineData("exec")] + [InlineData("command")] + [InlineData("expression")] + public async Task Script_PassesOptionWordsAndRestoresScope(string callKind) + { + var path = Path.GetTempFileName(); + SetVariable("cmd", new ShellText(path)); + SetVariable("text", new ShellText("two words")); + var scopeCount = Shell.VariableContainers.Count; + try + { + await File.WriteAllTextAsync(path, "return [$1, $2, $3]", TestContext.Current.CancellationToken); + const string invocation = "exec $cmd --name=$text -m 5"; + var parsed = StatementParser.ScriptParseResult.Parse(invocation); + Assert.False(parsed.Errors.HasErrors); + var arguments = Assert.IsType(Assert.Single(parsed.Statements)).Arguments; + var command = new CommandStatement(new Token(TokenType.Identifier, path, 0, path.Length)); + command.Arguments.AddRange(arguments); + var expression = new CommandExpression(command.CommandToken); + expression.Arguments.AddRange(arguments); + var state = callKind switch + { + "command" => await command.RunAsync(Shell, new(), CancellationToken.None), + "expression" => await expression.ExecuteCommandAsync(Shell, new(), CancellationToken.None), + _ => await Shell.RunCommandAsync(new(), invocation, CancellationToken.None), + }; + + Assert.False(state.IsError); + var values = Assert.IsType(state.Result).Value; + Assert.Equal("--name=two words", values[0].GetString()); + Assert.Equal("-m", values[1].GetString()); + Assert.Equal("5", values[2].GetString()); + Assert.Equal(scopeCount, Shell.VariableContainers.Count); + Assert.Null(Shell.CurrentScriptFileName); + } + finally + { + File.Delete(path); + } + } + + [Fact] + public async Task Exec_ShellWords_PreservePathsAndNegativeArguments() + { + var state = await Shell.RunCommandAsync(new(), "exec \"echo\" C:\\temp\\file.txt https://example.test/a -5", CancellationToken.None); + Assert.False(state.IsError); + Assert.Equal("C:\\temp\\file.txt https://example.test/a -5", Assert.IsType(state.Result).Text); + } + [Fact] public async Task Exec_EmptyCommandPath_Throws() { @@ -115,3 +233,22 @@ public async Task Exec_FailingCommandWithScriptContext_WrapsInPositionalExceptio Assert.Equal("script.csh", ex.FileName); } } + +[CosmosCommand("execbinding")] +internal class ExecBindingCommand : CosmosCommand +{ + [CosmosOption("text", "t")] + public string? Text { get; init; } + + [CosmosOption("flag", "f")] + public bool Flag { get; init; } + + [CosmosOption("max", "m")] + public int? Max { get; init; } + + public override Task ExecuteAsync(ShellInterpreter shell, CommandState commandState, string commandText, CancellationToken token) + { + commandState.Result = new ShellJson(System.Text.Json.JsonSerializer.SerializeToElement(new { text = this.Text, flag = this.Flag, max = this.Max })); + return Task.FromResult(commandState); + } +} diff --git a/CosmosDBShell.Tests/Parser/StatementParserEdgeTests.cs b/CosmosDBShell.Tests/Parser/StatementParserEdgeTests.cs index 9736af15..5e31fe78 100644 --- a/CosmosDBShell.Tests/Parser/StatementParserEdgeTests.cs +++ b/CosmosDBShell.Tests/Parser/StatementParserEdgeTests.cs @@ -49,6 +49,43 @@ public void Exec_WithArguments_ProducesExecStatement() Assert.IsType(Assert.Single(statements)); } + [Theory] + [InlineData("exec $cmd --flag; echo next")] + [InlineData("exec $cmd --flag\necho next")] + [InlineData("{ exec $cmd --flag }; echo next")] + [InlineData("exec $cmd --flag | echo next")] + public void Exec_Options_StopAtStatementBoundaries(string input) + { + var result = StatementParser.ScriptParseResult.Parse(input); + Assert.False(result.Errors.HasErrors); + var statement = result.Statements[0] switch + { + BlockStatement block => block.Statements[0], + PipeStatement pipe => pipe.Statements[0], + var first => first, + }; + var executed = Assert.IsType(statement); + Assert.Equal("flag", Assert.IsType(Assert.Single(executed.Arguments)).Name); + } + + [Theory] + [InlineData("exec $cmd --name=")] + [InlineData("exec $cmd --name:; echo next")] + public void Exec_MissingInlineOptionValue_ReportsParseError(string input) + { + var result = StatementParser.ScriptParseResult.Parse(input); + Assert.True(result.Errors.HasErrors); + } + + [Fact] + public void Exec_ToString_PreservesOptionsAndQuotedValues() + { + const string input = "exec $cmd --name='two words' -m 5"; + var result = StatementParser.ScriptParseResult.Parse(input); + Assert.False(result.Errors.HasErrors); + Assert.Equal("exec $cmd --name=\"two words\" -m 5", Assert.Single(result.Statements).ToString()); + } + [Fact] public void Def_WithParenParameters_ParsesParameters() { diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/CommandArgumentFormatter.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/CommandArgumentFormatter.cs index 35097efa..a98b787a 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/CommandArgumentFormatter.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/CommandArgumentFormatter.cs @@ -22,8 +22,7 @@ public static string Format(Expression argument) // remain separate AST arguments and are bound later using command metadata. if (argument is CommandOption option) { - var dashCount = Math.Max(1, option.NameToken.Start - option.MinusToken.Start); - var text = new string('-', dashCount) + option.Name; + var text = FormatOptionName(option); return option.Value == null ? text : $"{text}{option.SeparatorToken?.Value ?? "="}{Format(option.Value)}"; } @@ -41,4 +40,10 @@ public static string Format(Expression argument) return argument.ToString() ?? string.Empty; } + + internal static string FormatOptionName(CommandOption option) + { + var dashCount = Math.Max(1, option.NameToken.Start - option.MinusToken.Start); + return new string('-', dashCount) + option.Name; + } } diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/CommandOption.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/CommandOption.cs index ef8113ac..1be97a57 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/CommandOption.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/CommandOption.cs @@ -6,6 +6,7 @@ namespace Azure.Data.Cosmos.Shell.Parser; using System; using Azure.Data.Cosmos.Shell.Core; +using Azure.Data.Cosmos.Shell.Util; internal class CommandOption : Expression { @@ -31,10 +32,21 @@ public CommandOption(Token minusToken, Token nameToken, Token? separatorToken = public override int Length => (this.Value != null ? this.Value.Start + this.Value.Length : this.NameToken.Start + this.NameToken.Length) - this.MinusToken.Start; - public override Task EvaluateAsync(ShellInterpreter interpreter, CommandState currentState, CancellationToken cancellationToken) + public override async Task EvaluateAsync(ShellInterpreter interpreter, CommandState currentState, CancellationToken cancellationToken) { - // should never be called directly, only used in CommandStatement - throw new NotImplementedException(); + var text = CommandArgumentFormatter.FormatOptionName(this); + if (this.Value != null) + { + var value = await this.Value.EvaluateAsync(interpreter, currentState, cancellationToken); + if (value.ConvertShellObject(DataType.Text) is not string valueText) + { + throw new InvalidOperationException(MessageService.GetArgsString("statement_error_invalid_option_value", "option", this.Name)); + } + + text += (this.SeparatorToken?.Value ?? "=") + valueText; + } + + return new ShellText(text); } public override void Accept(IAstVisitor visitor) diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/ExecStatement.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/ExecStatement.cs index 6125a355..73f15ff3 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/ExecStatement.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/ExecStatement.cs @@ -138,10 +138,11 @@ public override async Task RunAsync(ShellInterpreter shell, Comman public override string ToString() { - var args = string.Join(" ", this.Arguments.Select(a => a.ToString())); + var args = string.Join(" ", this.Arguments.Select(CommandArgumentFormatter.Format)); + var command = CommandArgumentFormatter.Format(this.CommandExpression); return string.IsNullOrEmpty(args) - ? $"exec {this.CommandExpression}" - : $"exec {this.CommandExpression} {args}"; + ? $"exec {command}" + : $"exec {command} {args}"; } /// diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/StatementParser.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/StatementParser.cs index b2299056..c1129dfd 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/StatementParser.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/StatementParser.cs @@ -507,7 +507,12 @@ private void SkipWs() this.expressionParser.Current.Type != TokenType.CloseBrace && this.expressionParser.Current.Type != TokenType.Pipe) { - var arg = this.expressionParser.ParsePrimaryExpression(); + var arg = this.ParseCommandArgument(this.CreateCommandShellWordParser()); + if (arg == null) + { + break; + } + arguments.Add(arg); } @@ -947,83 +952,13 @@ private void SkipWs() this.expressionParser.Current.Type != TokenType.RedirectAppendError && !this.IsStderrRedirectStart()) { - var commandWordParser = this.CreateCommandShellWordParser(); - - // In command mode an argument is either a structured option (`-name`, `--name[=:]value`) - // or a "shell word": a maximal run of adjacent simple tokens concatenated as text. - // Strings, interpolated strings, $-variables and (expr)/[..]/{..} drop into expression - // mode unchanged so existing scripting features keep working. - if (this.expressionParser.Current.Type == TokenType.Minus && - commandWordParser.IsCommandOptionStart()) + var arg = this.ParseCommandArgument(this.CreateCommandShellWordParser()); + if (arg == null) { - var optionStartToken = this.expressionParser.Current; - this.expressionParser.Advance(); - - bool doubleDash = false; - if (!this.expressionParser.IsAtEnd && - this.expressionParser.Current != null && - this.expressionParser.Current.Type == TokenType.Minus) - { - doubleDash = true; - this.expressionParser.Advance(); - } - - if (this.expressionParser.IsAtEnd || - this.expressionParser.Current == null || - this.expressionParser.Current.Type != TokenType.Identifier) - { - this.ReportError( - MessageService.GetArgsString("statement_error_expected_option_name", "prefix", doubleDash ? "--" : "-"), - this.expressionParser.Current); - break; - } - - var optionNameToken = this.expressionParser.Current; - var optionName = optionNameToken.Value; - this.expressionParser.Advance(); - - Token? separatorToken = null; - Expression? optionValue = null; - if (!this.expressionParser.IsAtEnd && - this.expressionParser.Current != null && - (this.expressionParser.Current.Type == TokenType.Colon || - this.expressionParser.Current.Type == TokenType.Assignment)) - { - separatorToken = this.expressionParser.Current; - this.expressionParser.Advance(); - - if (!this.expressionParser.IsAtEnd && - this.expressionParser.Current != null) - { - optionValue = commandWordParser.ParseShellWord(); - - if (optionValue == null) - { - this.ReportError( - MessageService.GetArgsString("statement_error_invalid_option_value", "option", optionName), - this.expressionParser.Current); - } - } - else - { - this.ReportError( - MessageService.GetArgsString("statement_error_invalid_option_value", "option", optionName), - this.expressionParser.Current); - } - } - - command.Arguments.Add(new CommandOption(optionStartToken, optionNameToken, separatorToken, optionValue)); + break; } - else - { - var arg = commandWordParser.ParseShellWord(); - if (arg == null) - { - break; - } - command.Arguments.Add(arg); - } + command.Arguments.Add(arg); } while (!this.expressionParser.IsAtEnd && @@ -1164,6 +1099,57 @@ private void SkipWs() } } + private Expression? ParseCommandArgument(CommandShellWordParser commandWordParser) + { + if (this.expressionParser.Current?.Type != TokenType.Minus || + !commandWordParser.IsCommandOptionStart()) + { + return commandWordParser.ParseShellWord(); + } + + var optionStartToken = this.expressionParser.Current; + this.expressionParser.Advance(); + bool doubleDash = false; + if (!this.expressionParser.IsAtEnd && + this.expressionParser.Current?.Type == TokenType.Minus) + { + doubleDash = true; + this.expressionParser.Advance(); + } + + if (this.expressionParser.IsAtEnd || + this.expressionParser.Current == null || + this.expressionParser.Current.Type != TokenType.Identifier) + { + this.ReportError( + MessageService.GetArgsString("statement_error_expected_option_name", "prefix", doubleDash ? "--" : "-"), + this.expressionParser.Current); + return null; + } + + var optionNameToken = this.expressionParser.Current; + this.expressionParser.Advance(); + Token? separatorToken = null; + Expression? optionValue = null; + if (!this.expressionParser.IsAtEnd && + this.expressionParser.Current != null && + (this.expressionParser.Current.Type == TokenType.Colon || + this.expressionParser.Current.Type == TokenType.Assignment)) + { + separatorToken = this.expressionParser.Current; + this.expressionParser.Advance(); + optionValue = commandWordParser.ParseShellWord(); + if (optionValue == null) + { + this.ReportError( + MessageService.GetArgsString("statement_error_invalid_option_value", "option", optionNameToken.Value), + this.expressionParser.Current); + } + } + + return new CommandOption(optionStartToken, optionNameToken, separatorToken, optionValue); + } + private T? Safe(Func action) where T : class { diff --git a/README.md b/README.md index d25bce44..4780a0aa 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,8 @@ Parser nesting, expression tree depth, and active function/script calls have fix Script diagnostics preserve source files, runtime failure categories, and function/script call sites. The language server shares the runtime's control-flow and duplicate-parameter checks, recognizes document-local functions, and checks nested commands and options. Incorrect function argument counts produce usage exit code `2`. See [validation and errors](docs/programming.md#validation-and-errors). +Dynamic `exec` calls accept the same options and shell words as direct commands. Built-in commands bind options normally; functions and script files receive option-shaped arguments as positional text. See [exec](docs/programming.md#exec). + Loops and functions preserve JSON `null` values. Numeric conditions use the same zero/nonzero rule for shell values and JSON properties, including fractional numbers. See [value conversion rules](docs/programming.md#numbers). JSON strings use the same `+` concatenation rules as shell strings. A bare `return` is valid immediately before a closing block brace. Editor variable symbols distinguish case-sensitive names such as `$value` and `$Value`. diff --git a/docs/commands.md b/docs/commands.md index 1c4c6f13..749d5a70 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -795,6 +795,7 @@ Arguments: Notes: - If `expression` evaluates to an existing file path, it is executed as a `.csh` script. +- Arguments use direct-command shell-word and option syntax, including short options, `--name=value`, `--name:value`, and quoted or computed values. Built-in commands bind options normally; functions and scripts receive option-shaped words as positional text. Examples: @@ -802,6 +803,8 @@ Examples: $script = {path: "myscript.csh"} exec $script.path arg1 arg2 +exec "info" --db=mydb --con=mycontainer + for $file in (dir "*.csh") { exec $file.path } ``` diff --git a/docs/programming.md b/docs/programming.md index f7d15ed5..be1bdba4 100644 --- a/docs/programming.md +++ b/docs/programming.md @@ -304,6 +304,8 @@ Notes: - If the evaluated value is a file path that exists, the shell runs it as a `.csh` script. - Argument parsing stops at `;`, newline, `}`, or `|` (so you can chain with pipes). +- Arguments use the same shell-word and option parsing as direct commands, including `-m 5`, `--name=value`, `--name:value`, quoted values, and variable or parenthesized values. +- Built-in commands bind options using their normal metadata. Functions and script files receive option-shaped words as positional text, such as `--name=value`; values are evaluated once in the caller. Examples: From de1e953ce80d9abd2287a3f52fc790a033de851a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Tue, 6 Oct 2026 12:03:20 +0200 Subject: [PATCH 2/2] Reject empty inline command option values Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Parser/CommandExecutionTests.cs | 15 +++++++++++++++ .../Statement/CommandOption.cs | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/CosmosDBShell.Tests/Parser/CommandExecutionTests.cs b/CosmosDBShell.Tests/Parser/CommandExecutionTests.cs index 8c7a0c7e..3d307509 100644 --- a/CosmosDBShell.Tests/Parser/CommandExecutionTests.cs +++ b/CosmosDBShell.Tests/Parser/CommandExecutionTests.cs @@ -11,6 +11,7 @@ namespace CosmosShell.Tests.Parser; using Azure.Data.Cosmos.Shell.Commands; using Azure.Data.Cosmos.Shell.Core; using Azure.Data.Cosmos.Shell.Parser; +using Azure.Data.Cosmos.Shell.Util; /// /// Drives execution of and : @@ -160,6 +161,20 @@ public async Task Function_OptionWords_ArePositionalText(string invocation, stri Assert.Equal(expected, Assert.IsType(result).Text); } + [Theory] + [InlineData("=")] + [InlineData(":")] + public async Task Function_CommandExpressionMissingInlineOptionValue_ReportsError(string separator) + { + var exception = await Assert.ThrowsAsync(() => + Shell.RunCommandAsync( + new(), + $"def identity [value] {{ return $value }}; $result = (identity --name{separator})", + CancellationToken.None)); + + Assert.Equal(MessageService.GetArgsString("statement_error_invalid_option_value", "option", "name"), exception.Message); + } + [Theory] [InlineData("exec")] [InlineData("command")] diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/CommandOption.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/CommandOption.cs index 1be97a57..3e26bd14 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/CommandOption.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/Statement/CommandOption.cs @@ -35,6 +35,11 @@ public CommandOption(Token minusToken, Token nameToken, Token? separatorToken = public override async Task EvaluateAsync(ShellInterpreter interpreter, CommandState currentState, CancellationToken cancellationToken) { var text = CommandArgumentFormatter.FormatOptionName(this); + if (this.SeparatorToken != null && this.Value == null) + { + throw new InvalidOperationException(MessageService.GetArgsString("statement_error_invalid_option_value", "option", this.Name)); + } + if (this.Value != null) { var value = await this.Value.EvaluateAsync(interpreter, currentState, cancellationToken);