Browse Source

Add new flow-control commands for variables and policies scopes management

Add block() and endblock() commands offering the capability to create
new scopes for variables and/or policies.

Fixes: #20171
Marc Chevrier 3 years ago
parent
commit
44a2f3f332
66 changed files with 750 additions and 12 deletions
  1. 2 2
      Auxiliary/cmake-mode.el
  2. 2 2
      Auxiliary/vim/indent/cmake.vim
  3. 2 0
      Auxiliary/vim/syntax/cmake.vim
  4. 74 0
      Help/command/block.rst
  5. 1 0
      Help/command/cmake_language.rst
  6. 41 0
      Help/command/cmake_policy.rst
  7. 11 0
      Help/command/endblock.rst
  8. 11 6
      Help/command/set.rst
  9. 2 0
      Help/manual/cmake-commands.7.rst
  10. 5 0
      Help/release/dev/block-command.rst
  11. 2 0
      Source/CMakeLists.txt
  12. 200 0
      Source/cmBlockCommand.cxx
  13. 14 0
      Source/cmBlockCommand.h
  14. 3 2
      Source/cmCMakeLanguageCommand.cxx
  15. 6 0
      Source/cmCommands.cxx
  16. 11 0
      Source/cmListFileCache.cxx
  17. 1 0
      Tests/RunCMake/CMakeLists.txt
  18. 3 0
      Tests/RunCMake/block/CMakeLists.txt
  19. 1 0
      Tests/RunCMake/block/EndAlone-result.txt
  20. 4 0
      Tests/RunCMake/block/EndAlone-stderr.txt
  21. 1 0
      Tests/RunCMake/block/EndAlone.cmake
  22. 1 0
      Tests/RunCMake/block/EndAloneWithArgument-result.txt
  23. 4 0
      Tests/RunCMake/block/EndAloneWithArgument-stderr.txt
  24. 1 0
      Tests/RunCMake/block/EndAloneWithArgument.cmake
  25. 1 0
      Tests/RunCMake/block/EndMissing-result.txt
  26. 4 0
      Tests/RunCMake/block/EndMissing-stderr.txt
  27. 1 0
      Tests/RunCMake/block/EndMissing.cmake
  28. 9 0
      Tests/RunCMake/block/EndWithArgument-stderr.txt
  29. 2 0
      Tests/RunCMake/block/EndWithArgument.cmake
  30. 1 0
      Tests/RunCMake/block/InvalidArgument-result.txt
  31. 4 0
      Tests/RunCMake/block/InvalidArgument-stderr.txt
  32. 2 0
      Tests/RunCMake/block/InvalidArgument.cmake
  33. 1 0
      Tests/RunCMake/block/InvalidNesting1-result.txt
  34. 4 0
      Tests/RunCMake/block/InvalidNesting1-stderr.txt
  35. 6 0
      Tests/RunCMake/block/InvalidNesting1.cmake
  36. 1 0
      Tests/RunCMake/block/InvalidNesting2-result.txt
  37. 4 0
      Tests/RunCMake/block/InvalidNesting2-stderr.txt
  38. 6 0
      Tests/RunCMake/block/InvalidNesting2.cmake
  39. 1 0
      Tests/RunCMake/block/InvalidNesting3-result.txt
  40. 4 0
      Tests/RunCMake/block/InvalidNesting3-stderr.txt
  41. 5 0
      Tests/RunCMake/block/InvalidNesting3.cmake
  42. 1 0
      Tests/RunCMake/block/InvalidNesting4-result.txt
  43. 4 0
      Tests/RunCMake/block/InvalidNesting4-stderr.txt
  44. 5 0
      Tests/RunCMake/block/InvalidNesting4.cmake
  45. 1 0
      Tests/RunCMake/block/InvalidNesting5-result.txt
  46. 4 0
      Tests/RunCMake/block/InvalidNesting5-stderr.txt
  47. 5 0
      Tests/RunCMake/block/InvalidNesting5.cmake
  48. 1 0
      Tests/RunCMake/block/InvalidNesting6-result.txt
  49. 4 0
      Tests/RunCMake/block/InvalidNesting6-stderr.txt
  50. 5 0
      Tests/RunCMake/block/InvalidNesting6.cmake
  51. 1 0
      Tests/RunCMake/block/MissingArgument-result.txt
  52. 7 0
      Tests/RunCMake/block/MissingArgument-stderr.txt
  53. 2 0
      Tests/RunCMake/block/MissingArgument.cmake
  54. 22 0
      Tests/RunCMake/block/RunCMakeTest.cmake
  55. 30 0
      Tests/RunCMake/block/Scope-POLICIES.cmake
  56. 52 0
      Tests/RunCMake/block/Scope-VARIABLES.cmake
  57. 52 0
      Tests/RunCMake/block/Scope.cmake
  58. 78 0
      Tests/RunCMake/block/Workflows.cmake
  59. 1 0
      Tests/RunCMake/block/WrongArgument-result.txt
  60. 4 0
      Tests/RunCMake/block/WrongArgument-stderr.txt
  61. 2 0
      Tests/RunCMake/block/WrongArgument.cmake
  62. 1 0
      Tests/RunCMake/block/WrongScope-result.txt
  63. 4 0
      Tests/RunCMake/block/WrongScope-stderr.txt
  64. 2 0
      Tests/RunCMake/block/WrongScope.cmake
  65. 2 0
      Tests/RunCMake/cmake_language/RunCMakeTest.cmake
  66. 1 0
      bootstrap

+ 2 - 2
Auxiliary/cmake-mode.el

@@ -41,8 +41,8 @@ set the path with these commands:
   :group 'cmake)
 
 ;; Keywords
-(defconst cmake-keywords-block-open '("IF" "MACRO" "FOREACH" "ELSE" "ELSEIF" "WHILE" "FUNCTION"))
-(defconst cmake-keywords-block-close '("ENDIF" "ENDFOREACH" "ENDMACRO" "ELSE" "ELSEIF" "ENDWHILE" "ENDFUNCTION"))
+(defconst cmake-keywords-block-open '("BLOCK" "IF" "MACRO" "FOREACH" "ELSE" "ELSEIF" "WHILE" "FUNCTION"))
+(defconst cmake-keywords-block-close '("ENDBLOCK" "ENDIF" "ENDFOREACH" "ENDMACRO" "ELSE" "ELSEIF" "ENDWHILE" "ENDFUNCTION"))
 (defconst cmake-keywords
   (let ((kwds (append cmake-keywords-block-open cmake-keywords-block-close nil)))
     (delete-dups kwds)))

+ 2 - 2
Auxiliary/vim/indent/cmake.vim

@@ -59,8 +59,8 @@ fun! CMakeGetIndent(lnum)
 
   let cmake_closing_parens_line = '^\s*\()\+\)\s*$'
 
-  let cmake_indent_begin_regex = '^\s*\(IF\|MACRO\|FOREACH\|ELSE\|ELSEIF\|WHILE\|FUNCTION\)\s*('
-  let cmake_indent_end_regex = '^\s*\(ENDIF\|ENDFOREACH\|ENDMACRO\|ELSE\|ELSEIF\|ENDWHILE\|ENDFUNCTION\)\s*('
+  let cmake_indent_begin_regex = '^\s*\(BLOCK\|IF\|MACRO\|FOREACH\|ELSE\|ELSEIF\|WHILE\|FUNCTION\)\s*('
+  let cmake_indent_end_regex = '^\s*\(ENDBLOCK\|ENDIF\|ENDFOREACH\|ENDMACRO\|ELSE\|ELSEIF\|ENDWHILE\|ENDFUNCTION\)\s*('
 
   if this_line =~? cmake_closing_parens_line
     if previous_line !~? cmake_indent_open_regex

+ 2 - 0
Auxiliary/vim/syntax/cmake.vim

@@ -3832,6 +3832,7 @@ syn keyword cmakeCommand
             \ add_subdirectory
             \ add_test
             \ aux_source_directory
+            \ block
             \ break
             \ build_command
             \ cmake_host_system_information
@@ -3859,6 +3860,7 @@ syn keyword cmakeCommand
             \ define_property
             \ enable_language
             \ enable_testing
+            \ endblock
             \ endfunction
             \ endmacro
             \ execute_process

+ 74 - 0
Help/command/block.rst

@@ -0,0 +1,74 @@
+block
+-----
+
+.. versionadded:: 3.25
+
+Evaluate a group of commands with a dedicated variable and/or policy scope.
+
+.. code-block:: cmake
+
+  block([SCOPE_FOR (POLICIES|VARIABLES)] [PROPAGATE <var-name>...])
+    <commands>
+  endblock()
+
+All commands between ``block()`` and the matching :command:`endblock` are
+recorded without being invoked.  Once the :command:`endblock` is evaluated, the
+recorded list of commands is invoked inside the requested scopes, and, finally,
+the scopes created by ``block()`` command are removed.
+
+``SCOPE_FOR``
+  Specify which scopes must be created.
+
+  ``POLICIES``
+    Create a new policy scope. This is equivalent to
+    :command:`cmake_policy(PUSH)`.
+
+  ``VARIABLES``
+    Create a new variable scope.
+
+  If ``SCOPE_FOR`` is not specified, this is equivalent to:
+
+  .. code-block:: cmake
+
+    block(SCOPE_FOR VARIABLES POLICIES)
+
+``PROPAGATE``
+  When a variable scope is created by :command:`block` command, this option
+  set or unset the specified variables in the parent scope. This is equivalent
+  to :command:`set(PARENT_SCOPE)` or :command:`unset(PARENT_SCOPE)` commands.
+
+  .. code-block:: cmake
+
+    set(VAR1 "INIT1")
+    set(VAR2 "INIT2")
+
+    block(PROPAGATE VAR1 VAR2)
+      set(VAR1 "VALUE1")
+      unset(VAR2)
+    endblock()
+
+    # here, VAR1 holds value VALUE1 and VAR2 is unset
+
+  This option is only allowed when a variable scope is created. An error will
+  be raised in the other cases.
+
+When the ``block`` is local to a :command:`foreach` or :command:`while`
+command, the commands :command:`break` and :command:`continue` can be used
+inside this block.
+
+.. code-block:: cmake
+
+  while(TRUE)
+    block()
+       ...
+       # the break() command will terminate the while() command
+       break()
+    endblock()
+  endwhile()
+
+
+See Also
+^^^^^^^^
+
+  * :command:`endblock`
+  * :command:`cmake_policy`

+ 1 - 0
Help/command/cmake_language.rst

@@ -51,6 +51,7 @@ is equivalent to
   To ensure consistency of the code, the following commands are not allowed:
 
   * ``if`` / ``elseif`` / ``else`` / ``endif``
+  * ``block`` / ``endblock``
   * ``while`` / ``endwhile``
   * ``foreach`` / ``endforeach``
   * ``function`` / ``endfunction``

+ 41 - 0
Help/command/cmake_policy.rst

@@ -103,6 +103,47 @@ Calls to the :command:`cmake_minimum_required(VERSION)`,
 ``cmake_policy(VERSION)``, or ``cmake_policy(SET)`` commands
 influence only the current top of the policy stack.
 
+.. versionadded:: 3.25
+  The :command:`block` and :command:`endblock` commands offer a more flexible
+  and more secure way to manage the policy stack. The pop action is done
+  automatically when the :command:`endblock` command is executed, so it avoid
+  to call the :command:`cmake_policy(POP)` command before each
+  :command:`return` command.
+
+  .. code-block:: cmake
+
+    # stack management with cmake_policy()
+    function(my_func)
+      cmake_policy(PUSH)
+      cmake_policy(SET ...)
+      if (<cond1>)
+        ...
+        cmake_policy(POP)
+        return()
+      elseif(<cond2>)
+        ...
+        cmake_policy(POP)
+        return()
+      endif()
+      ...
+      cmake_policy(POP)
+    endfunction()
+
+    # stack management with block()/endblock()
+    function(my_func)
+      block(SCOPE_FOR POLICIES)
+        cmake_policy(SET ...)
+        if (<cond1>)
+          ...
+          return()
+        elseif(<cond2>)
+          ...
+          return()
+        endif()
+        ...
+      endblock()
+    endfunction()
+
 Commands created by the :command:`function` and :command:`macro`
 commands record policy settings when they are created and
 use the pre-record policies when they are invoked.  If the function or

+ 11 - 0
Help/command/endblock.rst

@@ -0,0 +1,11 @@
+endblock
+--------
+
+.. versionadded:: 3.25
+
+Ends a list of commands in a :command:`block` and removes the scopes
+created by the :command:`block` command.
+
+.. code-block:: cmake
+
+  endblock()

+ 11 - 6
Help/command/set.rst

@@ -22,12 +22,17 @@ Set Normal Variable
 Sets the given ``<variable>`` in the current function or directory scope.
 
 If the ``PARENT_SCOPE`` option is given the variable will be set in
-the scope above the current scope.  Each new directory or function
-creates a new scope.  This command will set the value of a variable
-into the parent directory or calling function (whichever is applicable
-to the case at hand). The previous state of the variable's value stays the
-same in the current scope (e.g., if it was undefined before, it is still
-undefined and if it had a value, it is still that value).
+the scope above the current scope.  Each new directory or :command:`function`
+command creates a new scope.  A scope can also be created with the
+:command:`block` command. This command will set the value of a variable into
+the parent directory, calling function or encompassing scope (whichever is
+applicable to the case at hand). The previous state of the variable's value
+stays the same in the current scope (e.g., if it was undefined before, it is
+still undefined and if it had a value, it is still that value).
+
+The :command:`block(PROPAGATE)` command can be used as an alternate method to
+:command:`set(PARENT_SCOPE)` and :command:`unset(PARENT_SCOPE)` commands to
+update the parent scope.
 
 Set Cache Entry
 ^^^^^^^^^^^^^^^

+ 2 - 0
Help/manual/cmake-commands.7.rst

@@ -15,6 +15,7 @@ These commands are always available.
 .. toctree::
    :maxdepth: 1
 
+   /command/block
    /command/break
    /command/cmake_host_system_information
    /command/cmake_language
@@ -26,6 +27,7 @@ These commands are always available.
    /command/continue
    /command/else
    /command/elseif
+   /command/endblock
    /command/endforeach
    /command/endfunction
    /command/endif

+ 5 - 0
Help/release/dev/block-command.rst

@@ -0,0 +1,5 @@
+block-command
+-------------
+
+* CMake language gains the commands :command:`block` and :command:`endblock` to
+  manage specific scopes (policy or variable) for group of commands.

+ 2 - 0
Source/CMakeLists.txt

@@ -571,6 +571,8 @@ set(SRCS
   cmFindProgramCommand.h
   cmForEachCommand.cxx
   cmForEachCommand.h
+  cmBlockCommand.cxx
+  cmBlockCommand.h
   cmFunctionBlocker.cxx
   cmFunctionBlocker.h
   cmFunctionCommand.cxx

+ 200 - 0
Source/cmBlockCommand.cxx

@@ -0,0 +1,200 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+
+#include "cmBlockCommand.h"
+
+#include <cstdint> // IWYU pragma: keep
+#include <utility>
+
+#include <cm/memory>
+#include <cm/optional>
+#include <cm/string_view>
+#include <cmext/enum_set>
+#include <cmext/string_view>
+
+#include "cmArgumentParser.h"
+#include "cmArgumentParserTypes.h"
+#include "cmExecutionStatus.h"
+#include "cmFunctionBlocker.h"
+#include "cmListFileCache.h"
+#include "cmMakefile.h"
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
+
+namespace {
+enum class ScopeType : std::uint8_t
+{
+  VARIABLES,
+  POLICIES
+};
+using ScopeSet = cm::enum_set<ScopeType>;
+
+class BlockScopePushPop
+{
+public:
+  BlockScopePushPop(cmMakefile* m, const ScopeSet& scopes);
+  ~BlockScopePushPop() = default;
+
+  BlockScopePushPop(const BlockScopePushPop&) = delete;
+  BlockScopePushPop& operator=(const BlockScopePushPop&) = delete;
+
+private:
+  std::unique_ptr<cmMakefile::PolicyPushPop> PolicyScope;
+  std::unique_ptr<cmMakefile::VariablePushPop> VariableScope;
+};
+
+BlockScopePushPop::BlockScopePushPop(cmMakefile* mf, const ScopeSet& scopes)
+{
+  if (scopes.contains(ScopeType::POLICIES)) {
+    this->PolicyScope = cm::make_unique<cmMakefile::PolicyPushPop>(mf);
+  }
+  if (scopes.contains(ScopeType::VARIABLES)) {
+    this->VariableScope = cm::make_unique<cmMakefile::VariablePushPop>(mf);
+  }
+}
+
+class cmBlockFunctionBlocker : public cmFunctionBlocker
+{
+public:
+  cmBlockFunctionBlocker(cmMakefile* mf, const ScopeSet& scopes,
+                         std::vector<std::string> variableNames);
+  ~cmBlockFunctionBlocker() override;
+
+  cm::string_view StartCommandName() const override { return "block"_s; }
+  cm::string_view EndCommandName() const override { return "endblock"_s; }
+
+  bool EndCommandSupportsArguments() const override { return false; }
+
+  bool ArgumentsMatch(cmListFileFunction const& lff,
+                      cmMakefile& mf) const override;
+
+  bool Replay(std::vector<cmListFileFunction> functions,
+              cmExecutionStatus& inStatus) override;
+
+private:
+  cmMakefile* Makefile;
+  BlockScopePushPop BlockScope;
+  std::vector<std::string> VariableNames;
+};
+
+cmBlockFunctionBlocker::cmBlockFunctionBlocker(
+  cmMakefile* const mf, const ScopeSet& scopes,
+  std::vector<std::string> variableNames)
+  : Makefile{ mf }
+  , BlockScope{ mf, scopes }
+  , VariableNames{ std::move(variableNames) }
+{
+}
+
+cmBlockFunctionBlocker::~cmBlockFunctionBlocker()
+{
+  for (auto const& varName : this->VariableNames) {
+    if (this->Makefile->IsNormalDefinitionSet(varName)) {
+      this->Makefile->RaiseScope(varName,
+                                 this->Makefile->GetDefinition(varName));
+    } else {
+      // unset variable in parent scope
+      this->Makefile->RaiseScope(varName, nullptr);
+    }
+  }
+}
+
+bool cmBlockFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff,
+                                            cmMakefile&) const
+{
+  // no arguments expected for endblock()
+  // but this method should not be called because EndCommandHasArguments()
+  // returns false.
+  return lff.Arguments().empty();
+}
+
+bool cmBlockFunctionBlocker::Replay(std::vector<cmListFileFunction> functions,
+                                    cmExecutionStatus& inStatus)
+{
+  auto& mf = inStatus.GetMakefile();
+
+  // Invoke all the functions that were collected in the block.
+  for (cmListFileFunction const& fn : functions) {
+    cmExecutionStatus status(mf);
+    mf.ExecuteCommand(fn, status);
+    if (status.GetReturnInvoked()) {
+      inStatus.SetReturnInvoked();
+      return true;
+    }
+    if (status.GetBreakInvoked()) {
+      inStatus.SetBreakInvoked();
+      return true;
+    }
+    if (status.GetContinueInvoked()) {
+      inStatus.SetContinueInvoked();
+      return true;
+    }
+    if (cmSystemTools::GetFatalErrorOccurred()) {
+      return true;
+    }
+  }
+  return true;
+}
+
+} // anonymous namespace
+
+bool cmBlockCommand(std::vector<std::string> const& args,
+                    cmExecutionStatus& status)
+{
+  struct Arguments : public ArgumentParser::ParseResult
+  {
+    cm::optional<ArgumentParser::NonEmpty<std::vector<std::string>>> ScopeFor;
+    ArgumentParser::MaybeEmpty<std::vector<std::string>> Propagate;
+  };
+  static auto const parser = cmArgumentParser<Arguments>{}
+                               .Bind("SCOPE_FOR"_s, &Arguments::ScopeFor)
+                               .Bind("PROPAGATE"_s, &Arguments::Propagate);
+  std::vector<std::string> unrecognizedArguments;
+  auto parsedArgs = parser.Parse(args, &unrecognizedArguments);
+
+  if (!unrecognizedArguments.empty()) {
+    status.SetError(cmStrCat("called with unsupported argument \"",
+                             unrecognizedArguments[0], '"'));
+    cmSystemTools::SetFatalErrorOccurred();
+    return false;
+  }
+
+  if (parsedArgs.MaybeReportError(status.GetMakefile())) {
+    cmSystemTools::SetFatalErrorOccurred();
+    return true;
+  }
+
+  ScopeSet scopes;
+
+  if (parsedArgs.ScopeFor) {
+    for (auto const& scope : *parsedArgs.ScopeFor) {
+      if (scope == "VARIABLES"_s) {
+        scopes.insert(ScopeType::VARIABLES);
+        continue;
+      }
+      if (scope == "POLICIES"_s) {
+        scopes.insert(ScopeType::POLICIES);
+        continue;
+      }
+      status.SetError(cmStrCat("SCOPE_FOR unsupported scope \"", scope, '"'));
+      cmSystemTools::SetFatalErrorOccurred();
+      return false;
+    }
+  } else {
+    scopes = { ScopeType::VARIABLES, ScopeType::POLICIES };
+  }
+  if (!scopes.contains(ScopeType::VARIABLES) &&
+      !parsedArgs.Propagate.empty()) {
+    status.SetError(
+      "PROPAGATE cannot be specified without a new scope for VARIABLES");
+    cmSystemTools::SetFatalErrorOccurred();
+    return false;
+  }
+
+  // create a function blocker
+  auto fb = cm::make_unique<cmBlockFunctionBlocker>(
+    &status.GetMakefile(), scopes, parsedArgs.Propagate);
+  status.GetMakefile().AddFunctionBlocker(std::move(fb));
+
+  return true;
+}

+ 14 - 0
Source/cmBlockCommand.h

@@ -0,0 +1,14 @@
+/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
+   file Copyright.txt or https://cmake.org/licensing for details.  */
+#pragma once
+
+#include "cmConfigure.h" // IWYU pragma: keep
+
+#include <string>
+#include <vector>
+
+class cmExecutionStatus;
+
+/// Starts block() ... endblock() block
+bool cmBlockCommand(std::vector<std::string> const& args,
+                    cmExecutionStatus& status);

+ 3 - 2
Source/cmCMakeLanguageCommand.cxx

@@ -36,13 +36,14 @@ bool FatalError(cmExecutionStatus& status, std::string const& error)
   return false;
 }
 
-std::array<cm::static_string_view, 12> InvalidCommands{
+std::array<cm::static_string_view, 14> InvalidCommands{
   { // clang-format off
   "function"_s, "endfunction"_s,
   "macro"_s, "endmacro"_s,
   "if"_s, "elseif"_s, "else"_s, "endif"_s,
   "while"_s, "endwhile"_s,
-  "foreach"_s, "endforeach"_s
+  "foreach"_s, "endforeach"_s,
+  "block"_s, "endblock"_s
   } // clang-format on
 };
 

+ 6 - 0
Source/cmCommands.cxx

@@ -14,6 +14,7 @@
 #include "cmAddLibraryCommand.h"
 #include "cmAddSubDirectoryCommand.h"
 #include "cmAddTestCommand.h"
+#include "cmBlockCommand.h"
 #include "cmBreakCommand.h"
 #include "cmBuildCommand.h"
 #include "cmCMakeLanguageCommand.h"
@@ -126,6 +127,7 @@ void GetScriptingCommands(cmState* state)
   state->AddFlowControlCommand("macro", cmMacroCommand);
   state->AddFlowControlCommand("return", cmReturnCommand);
   state->AddFlowControlCommand("while", cmWhileCommand);
+  state->AddFlowControlCommand("block", cmBlockCommand);
 
   state->AddBuiltinCommand("cmake_language", cmCMakeLanguageCommand);
   state->AddBuiltinCommand("cmake_minimum_required", cmCMakeMinimumRequired);
@@ -198,6 +200,10 @@ void GetScriptingCommands(cmState* state)
     "An ENDWHILE command was found outside of a proper "
     "WHILE ENDWHILE structure. Or its arguments did not "
     "match the opening WHILE command.");
+  state->AddUnexpectedFlowControlCommand(
+    "endblock",
+    "An ENDBLOCK command was found outside of a proper "
+    "BLOCK ENDBLOCK structure.");
 
 #if !defined(CMAKE_BOOTSTRAP)
   state->AddBuiltinCommand("cmake_host_system_information",

+ 11 - 0
Source/cmListFileCache.cxx

@@ -347,6 +347,7 @@ enum class NestingStateEnum
   Foreach,
   Function,
   Macro,
+  Block
 };
 
 struct NestingState
@@ -434,6 +435,16 @@ cm::optional<cmListFileContext> cmListFileParser::CheckNesting() const
         return cmListFileContext::FromListFileFunction(func, this->FileName);
       }
       stack.pop_back();
+    } else if (name == "block") {
+      stack.push_back({
+        NestingStateEnum::Block,
+        cmListFileContext::FromListFileFunction(func, this->FileName),
+      });
+    } else if (name == "endblock") {
+      if (!TopIs(stack, NestingStateEnum::Block)) {
+        return cmListFileContext::FromListFileFunction(func, this->FileName);
+      }
+      stack.pop_back();
     }
   }
 

+ 1 - 0
Tests/RunCMake/CMakeLists.txt

@@ -456,6 +456,7 @@ add_RunCMake_test(find_path)
 add_RunCMake_test(find_program -DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME})
 add_RunCMake_test(foreach)
 add_RunCMake_test(function)
+add_RunCMake_test(block)
 add_RunCMake_test(get_filename_component)
 add_RunCMake_test(get_property)
 add_RunCMake_test(if)

+ 3 - 0
Tests/RunCMake/block/CMakeLists.txt

@@ -0,0 +1,3 @@
+cmake_minimum_required(VERSION 3.3...3.25)
+project(${RunCMake_TEST} NONE)
+include(${RunCMake_TEST}.cmake)

+ 1 - 0
Tests/RunCMake/block/EndAlone-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/EndAlone-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at EndAlone.cmake:[0-9]+ \(endblock\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 1 - 0
Tests/RunCMake/block/EndAlone.cmake

@@ -0,0 +1 @@
+endblock()

+ 1 - 0
Tests/RunCMake/block/EndAloneWithArgument-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/EndAloneWithArgument-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at EndAloneWithArgument.cmake:[0-9]+ \(endblock\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 1 - 0
Tests/RunCMake/block/EndAloneWithArgument.cmake

@@ -0,0 +1 @@
+endblock(WRONG_ARG)

+ 1 - 0
Tests/RunCMake/block/EndMissing-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/EndMissing-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at EndMissing.cmake:[0-9]+ \(block\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 1 - 0
Tests/RunCMake/block/EndMissing.cmake

@@ -0,0 +1 @@
+block()

+ 9 - 0
Tests/RunCMake/block/EndWithArgument-stderr.txt

@@ -0,0 +1,9 @@
+CMake Warning \(dev\) in EndWithArgument.cmake:
+  A logical block closing on the line
+
+  .+/Tests/RunCMake/block/EndWithArgument.cmake:[0-9]+ \(endblock\)
+
+  has unexpected arguments.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)
+This warning is for project developers.  Use -Wno-dev to suppress it.

+ 2 - 0
Tests/RunCMake/block/EndWithArgument.cmake

@@ -0,0 +1,2 @@
+block()
+endblock(END_ARG)

+ 1 - 0
Tests/RunCMake/block/InvalidArgument-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/InvalidArgument-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at InvalidArgument.cmake:[0-9]+ \(block\):
+  block PROPAGATE cannot be specified without a new scope for VARIABLES
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 2 - 0
Tests/RunCMake/block/InvalidArgument.cmake

@@ -0,0 +1,2 @@
+block(SCOPE_FOR POLICIES PROPAGATE VAR1)
+endblock()

+ 1 - 0
Tests/RunCMake/block/InvalidNesting1-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/InvalidNesting1-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at InvalidNesting1.cmake:[0-9]+ \(else\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 6 - 0
Tests/RunCMake/block/InvalidNesting1.cmake

@@ -0,0 +1,6 @@
+
+if (TRUE)
+  block()
+else()
+  endblock()
+endif()

+ 1 - 0
Tests/RunCMake/block/InvalidNesting2-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/InvalidNesting2-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at InvalidNesting2.cmake:[0-9]+ \(endblock\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 6 - 0
Tests/RunCMake/block/InvalidNesting2.cmake

@@ -0,0 +1,6 @@
+
+block()
+if (TRUE)
+elseif(FALSE)
+endblock()
+endif()

+ 1 - 0
Tests/RunCMake/block/InvalidNesting3-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/InvalidNesting3-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at InvalidNesting3.cmake:[0-9]+ \(endwhile\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 5 - 0
Tests/RunCMake/block/InvalidNesting3.cmake

@@ -0,0 +1,5 @@
+
+while(TRUE)
+block()
+endwhile()
+endblock()

+ 1 - 0
Tests/RunCMake/block/InvalidNesting4-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/InvalidNesting4-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at InvalidNesting4.cmake:[0-9]+ \(endblock\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 5 - 0
Tests/RunCMake/block/InvalidNesting4.cmake

@@ -0,0 +1,5 @@
+
+block()
+foreach(item IN ITEMS A B)
+endblock()
+endforeach()

+ 1 - 0
Tests/RunCMake/block/InvalidNesting5-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/InvalidNesting5-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at InvalidNesting5.cmake:[0-9]+ \(endfunction\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 5 - 0
Tests/RunCMake/block/InvalidNesting5.cmake

@@ -0,0 +1,5 @@
+
+function(FUNC)
+  block()
+endfunction()
+endblock()

+ 1 - 0
Tests/RunCMake/block/InvalidNesting6-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/InvalidNesting6-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at InvalidNesting6.cmake:[0-9]+ \(endblock\):
+  Flow control statements are not properly nested.
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 5 - 0
Tests/RunCMake/block/InvalidNesting6.cmake

@@ -0,0 +1,5 @@
+
+  block()
+macro(FUNC)
+endblock()
+endmacro()

+ 1 - 0
Tests/RunCMake/block/MissingArgument-result.txt

@@ -0,0 +1 @@
+1

+ 7 - 0
Tests/RunCMake/block/MissingArgument-stderr.txt

@@ -0,0 +1,7 @@
+CMake Error at MissingArgument.cmake:[0-9]+ \(block\):
+  Error after keyword "SCOPE_FOR":
+
+    missing required value
+
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 2 - 0
Tests/RunCMake/block/MissingArgument.cmake

@@ -0,0 +1,2 @@
+block(SCOPE_FOR)
+endblock()

+ 22 - 0
Tests/RunCMake/block/RunCMakeTest.cmake

@@ -0,0 +1,22 @@
+include(RunCMake)
+
+run_cmake(WrongArgument)
+run_cmake(InvalidArgument)
+run_cmake(MissingArgument)
+run_cmake(WrongScope)
+run_cmake(EndMissing)
+run_cmake(EndWithArgument)
+run_cmake(EndAlone)
+run_cmake(EndAloneWithArgument)
+
+run_cmake(InvalidNesting1)
+run_cmake(InvalidNesting2)
+run_cmake(InvalidNesting3)
+run_cmake(InvalidNesting4)
+run_cmake(InvalidNesting5)
+run_cmake(InvalidNesting6)
+
+run_cmake(Scope)
+run_cmake(Scope-VARIABLES)
+run_cmake(Scope-POLICIES)
+run_cmake(Workflows)

+ 30 - 0
Tests/RunCMake/block/Scope-POLICIES.cmake

@@ -0,0 +1,30 @@
+
+set(VAR1 "OUTER1")
+set(VAR2 "OUTER2")
+
+cmake_policy(SET CMP0139 NEW)
+
+# create a block with a new scope for policies
+block(SCOPE_FOR POLICIES)
+  set(VAR1 "INNER1")
+  unset(VAR2)
+  set(VAR3 "INNER3")
+
+  cmake_policy(SET CMP0139 OLD)
+endblock()
+
+# check final values for variables
+if(NOT DEFINED VAR1 OR NOT VAR1 STREQUAL "INNER1")
+  message(SEND_ERROR "block/endblock: VAR1 has unexpected value: ${VAR1}")
+endif()
+if(DEFINED VAR2)
+  message(SEND_ERROR "block/endblock: VAR2 is unexpectedly defined: ${VAR2}")
+endif()
+if(NOT DEFINED VAR3 OR NOT VAR3 STREQUAL "INNER3")
+  message(SEND_ERROR "block/endblock: VAR3 has unexpected value: ${VAR3}")
+endif()
+
+cmake_policy(GET CMP0139 CMP0139_STATUS)
+if(NOT CMP0139_STATUS STREQUAL "NEW")
+    message(SEND_ERROR "block/endblock: CMP0139 has unexpected value: ${CMP0139_STATUS}")
+endif()

+ 52 - 0
Tests/RunCMake/block/Scope-VARIABLES.cmake

@@ -0,0 +1,52 @@
+
+set(VAR1 "OUTER1")
+set(VAR2 "OUTER2")
+set(VAR3 "OUTER3")
+set(VAR4 "OUTER4")
+set(VAR5 "OUTER5")
+
+set(VAR6 "CACHE6" CACHE STRING "")
+set(VAR6 "OUTER6")
+
+cmake_policy(SET CMP0139 NEW)
+
+# create a block with a new scope for variables
+block(SCOPE_FOR VARIABLES PROPAGATE VAR3 VAR4 VAR5 VAR6 VAR7)
+  set(VAR1 "INNER1")
+  set(VAR2 "INNER2" PARENT_SCOPE)
+  set(VAR3 "INNER3")
+  unset(VAR4)
+  unset(VAR6)
+  set(VAR7 "INNER7")
+
+  cmake_policy(SET CMP0139 OLD)
+endblock()
+
+# check final values for variables
+if(NOT DEFINED VAR1 OR NOT VAR1 STREQUAL "OUTER1")
+  message(SEND_ERROR "block/endblock: VAR1 has unexpected value: ${VAR1}")
+endif()
+if(NOT DEFINED VAR2 OR NOT VAR2 STREQUAL "INNER2")
+  message(SEND_ERROR "block/endblock: VAR2 has unexpected value: ${VAR2}")
+endif()
+if(NOT DEFINED VAR3 OR NOT VAR3 STREQUAL "INNER3")
+  message(SEND_ERROR "block/endblock: VAR3 has unexpected value: ${VAR3}")
+endif()
+if(DEFINED VAR4)
+  message(SEND_ERROR "block/endblock: VAR4 is unexpectedly defined: ${VAR4}")
+endif()
+if(NOT DEFINED VAR5 OR NOT VAR5 STREQUAL "OUTER5")
+  message(SEND_ERROR "block/endblock: VAR5 has unexpected value: ${VAR5}")
+endif()
+unset(VAR6 CACHE)
+if (DEFINED VAR6)
+  message(SEND_ERROR "block/endblock: VAR6 is unexpectedly defined: ${VAR6}")
+endif()
+if(NOT DEFINED VAR7 OR NOT VAR7 STREQUAL "INNER7")
+  message(SEND_ERROR "block/endblock: VAR7 has unexpected value: ${VAR7}")
+endif()
+
+cmake_policy(GET CMP0139 CMP0139_STATUS)
+if(NOT CMP0139_STATUS STREQUAL "OLD")
+    message(SEND_ERROR "block/endblock: CMP0139 has unexpected value: ${CMP0139_STATUS}")
+endif()

+ 52 - 0
Tests/RunCMake/block/Scope.cmake

@@ -0,0 +1,52 @@
+
+set(VAR1 "OUTER1")
+set(VAR2 "OUTER2")
+set(VAR3 "OUTER3")
+set(VAR4 "OUTER4")
+set(VAR5 "OUTER5")
+
+set(VAR6 "CACHE6" CACHE STRING "")
+set(VAR6 "OUTER6")
+
+cmake_policy(SET CMP0139 NEW)
+
+# create a block with a new scope for variables and policies
+block(PROPAGATE VAR3 VAR4 VAR5 VAR6 VAR7)
+  set(VAR1 "INNER1")
+  set(VAR2 "INNER2" PARENT_SCOPE)
+  set(VAR3 "INNER3")
+  unset(VAR4)
+  unset(VAR6)
+  set(VAR7 "INNER7")
+
+  cmake_policy(SET CMP0139 OLD)
+endblock()
+
+# check final values for variables
+if(NOT DEFINED VAR1 OR NOT VAR1 STREQUAL "OUTER1")
+  message(SEND_ERROR "block/endblock: VAR1 has unexpected value: ${VAR1}")
+endif()
+if(NOT DEFINED VAR2 OR NOT VAR2 STREQUAL "INNER2")
+  message(SEND_ERROR "block/endblock: VAR2 has unexpected value: ${VAR2}")
+endif()
+if(NOT DEFINED VAR3 OR NOT VAR3 STREQUAL "INNER3")
+  message(SEND_ERROR "block/endblock: VAR3 has unexpected value: ${VAR3}")
+endif()
+if(DEFINED VAR4)
+  message(SEND_ERROR "block/endblock: VAR4 is unexpectedly defined: ${VAR4}")
+endif()
+if(NOT DEFINED VAR5 OR NOT VAR5 STREQUAL "OUTER5")
+  message(SEND_ERROR "block/endblock: VAR5 has unexpected value: ${VAR5}")
+endif()
+unset(VAR6 CACHE)
+if (DEFINED VAR6)
+  message(SEND_ERROR "block/endblock: VAR6 is unexpectedly defined: ${VAR6}")
+endif()
+if(NOT DEFINED VAR7 OR NOT VAR7 STREQUAL "INNER7")
+  message(SEND_ERROR "block/endblock: VAR6 has unexpected value: ${VAR7}")
+endif()
+
+cmake_policy(GET CMP0139 CMP0139_STATUS)
+if(NOT CMP0139_STATUS STREQUAL "NEW")
+    message(SEND_ERROR "block/endblock: CMP0139 has unexpected value: ${CMP0139_STATUS}")
+endif()

+ 78 - 0
Tests/RunCMake/block/Workflows.cmake

@@ -0,0 +1,78 @@
+
+set(VAR1 "OUTER1")
+set(VAR2 "OUTER2")
+set(VAR3 "OUTER3")
+
+while (TRUE)
+  # create a block with a new scope for variables
+  block(SCOPE_FOR VARIABLES PROPAGATE VAR3)
+    set(VAR2 "INNER2" PARENT_SCOPE)
+    set(VAR3 "INNER3")
+    break()
+  endblock()
+endwhile()
+
+# check final values for variables
+if(NOT DEFINED VAR1 OR NOT VAR1 STREQUAL "OUTER1")
+  message(SEND_ERROR "block/endblock: VAR1 has unexpected value: ${VAR1}")
+endif()
+if(NOT DEFINED VAR2 OR NOT VAR2 STREQUAL "INNER2")
+  message(SEND_ERROR "block/endblock: VAR2 has unexpected value: ${VAR2}")
+endif()
+if(NOT DEFINED VAR3 OR NOT VAR3 STREQUAL "INNER3")
+  message(SEND_ERROR "block/endblock: VAR3 has unexpected value: ${VAR3}")
+endif()
+
+
+
+set(VAR1 "OUTER1")
+set(VAR2 "OUTER2")
+set(VAR3 "OUTER3")
+
+function (OUTER)
+  # create a block with a new scope for variables
+  block(SCOPE_FOR VARIABLES PROPAGATE VAR3)
+    set(VAR2 "INNER2" PARENT_SCOPE)
+    set(VAR3 "INNER3")
+    return()
+  endblock()
+  set(VAR1 "INNER1" PARENT_SCOPE)
+endfunction()
+outer()
+
+# check final values for variables
+if(NOT DEFINED VAR1 OR NOT VAR1 STREQUAL "OUTER1")
+  message(SEND_ERROR "block/endblock: VAR1 has unexpected value: ${VAR1}")
+endif()
+if(NOT DEFINED VAR2 OR NOT VAR2 STREQUAL "OUTER2")
+  message(SEND_ERROR "block/endblock: VAR2 has unexpected value: ${VAR2}")
+endif()
+if(NOT DEFINED VAR3 OR NOT VAR3 STREQUAL "OUTER3")
+  message(SEND_ERROR "block/endblock: VAR3 has unexpected value: ${VAR3}")
+endif()
+
+
+
+set(VAR1 "OUTER1")
+set(VAR2 "OUTER2")
+set(VAR3 "OUTER3")
+
+foreach (id IN ITEMS 1 2 3)
+  # create a block with a new scope for variables
+  block(SCOPE_FOR VARIABLES PROPAGATE VAR${id})
+    set(VAR${id} "INNER${id}")
+    continue()
+    set(VAR${id} "BAD${id}")
+  endblock()
+endforeach()
+
+# check final values for variables
+if(NOT DEFINED VAR1 OR NOT VAR1 STREQUAL "INNER1")
+  message(SEND_ERROR "block/endblock: VAR1 has unexpected value: ${VAR1}")
+endif()
+if(NOT DEFINED VAR2 OR NOT VAR2 STREQUAL "INNER2")
+  message(SEND_ERROR "block/endblock: VAR2 has unexpected value: ${VAR2}")
+endif()
+if(NOT DEFINED VAR3 OR NOT VAR3 STREQUAL "INNER3")
+  message(SEND_ERROR "block/endblock: VAR3 has unexpected value: ${VAR3}")
+endif()

+ 1 - 0
Tests/RunCMake/block/WrongArgument-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/WrongArgument-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at WrongArgument.cmake:[0-9]+ \(block\):
+  block called with unsupported argument "WRONG_ARG"
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 2 - 0
Tests/RunCMake/block/WrongArgument.cmake

@@ -0,0 +1,2 @@
+block(WRONG_ARG)
+endblock()

+ 1 - 0
Tests/RunCMake/block/WrongScope-result.txt

@@ -0,0 +1 @@
+1

+ 4 - 0
Tests/RunCMake/block/WrongScope-stderr.txt

@@ -0,0 +1,4 @@
+CMake Error at WrongScope.cmake:[0-9]+ \(block\):
+  block SCOPE_FOR unsupported scope "WRONG_SCOPE"
+Call Stack \(most recent call first\):
+  CMakeLists.txt:[0-9]+ \(include\)

+ 2 - 0
Tests/RunCMake/block/WrongScope.cmake

@@ -0,0 +1,2 @@
+block(SCOPE_FOR WRONG_SCOPE)
+endblock()

+ 2 - 0
Tests/RunCMake/cmake_language/RunCMakeTest.cmake

@@ -8,6 +8,7 @@ foreach(command IN ITEMS
     "if" "elseif" "else" "endif"
     "while" "endwhile"
     "foreach" "endforeach"
+    "block" "endblock"
     )
   message(STATUS "Running call_invalid_command for ${command}...")
   run_cmake_with_options(call_invalid_command -Dcommand=${command})
@@ -42,6 +43,7 @@ foreach(command IN ITEMS
     "if" "elseif" "else" "endif"
     "while" "endwhile"
     "foreach" "endforeach"
+    "block" "endblock"
     "return"
     )
   message(STATUS "Running defer_call_invalid_command for ${command}...")

+ 1 - 0
bootstrap

@@ -301,6 +301,7 @@ CMAKE_CXX_SOURCES="\
   cmBinUtilsWindowsPEDumpbinGetRuntimeDependenciesTool \
   cmBinUtilsWindowsPELinker \
   cmBinUtilsWindowsPEObjdumpGetRuntimeDependenciesTool \
+  cmBlockCommand \
   cmBreakCommand \
   cmBuildCommand \
   cmCMakeLanguageCommand \