ART: Immutable `variables` dictionary in Checker

Python's lack of read-only references makes passing state information
to other functions unsafe. This patch adds an immutable dictionary
class to Checker and uses it when passing around current values of
variables.

Change-Id: I54f2eac54d4d59e16daa74364e6d91a6cc953f6f
diff --git a/tools/checker/match/file.py b/tools/checker/match/file.py
index 2ed4aa7..116fe9a 100644
--- a/tools/checker/match/file.py
+++ b/tools/checker/match/file.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from common.immutables                import ImmutableDict
 from common.logger                    import Logger
 from file_format.c1visualizer.struct  import C1visualizerFile, C1visualizerPass
 from file_format.checker.struct       import CheckerFile, TestCase, TestAssertion
@@ -112,7 +113,7 @@
       responsible for running the checks in the right order and scope, and
       for propagating the variable state between the check lines.
   """
-  varState = {}
+  varState = ImmutableDict()
   checkLines = checkGroup.assertions
   outputLines = outputGroup.body
   startLineNo = outputGroup.startLineNo
diff --git a/tools/checker/match/line.py b/tools/checker/match/line.py
index 2097430..711d814 100644
--- a/tools/checker/match/line.py
+++ b/tools/checker/match/line.py
@@ -32,20 +32,16 @@
   return splitExpressions
 
 def matchWords(checkerWord, stringWord, variables, pos):
-  """ Attempts to match a list of RegexExpressions against a string. 
+  """ Attempts to match a list of RegexExpressions against a string.
       Returns updated variable dictionary if successful and None otherwise.
   """
-  # Create own copy of the variable dictionary, otherwise updates would change
-  # the caller's state.
-  variables = dict(variables)
-
   for expression in checkerWord:
     # If `expression` is a variable reference, replace it with the value.
     if expression.variant == RegexExpression.Variant.VarRef:
       if expression.name in variables:
         pattern = re.escape(variables[expression.name])
       else:
-        Logger.testFailed("Multiple definitions of variable \"{}\"".format(expression.name), 
+        Logger.testFailed("Multiple definitions of variable \"{}\"".format(expression.name),
                           pos.fileName, pos.lineNo)
     else:
       pattern = expression.pattern
@@ -59,9 +55,9 @@
     # If `expression` was a variable definition, set the variable's value.
     if expression.variant == RegexExpression.Variant.VarDef:
       if expression.name not in variables:
-        variables[expression.name] = stringWord[:match.end()]
+        variables = variables.copyWith(expression.name, stringWord[:match.end()])
       else:
-        Logger.testFailed("Multiple definitions of variable \"{}\"".format(expression.name), 
+        Logger.testFailed("Multiple definitions of variable \"{}\"".format(expression.name),
                           pos.fileName, pos.lineNo)
 
     # Move cursor by deleting the matched characters.
@@ -70,7 +66,7 @@
   # Make sure the entire word matched, i.e. `stringWord` is empty.
   if stringWord:
     return None
-  
+
   return variables
 
 def MatchLines(checkerLine, stringLine, variables):
diff --git a/tools/checker/match/test.py b/tools/checker/match/test.py
index 0215f50..97725ad 100644
--- a/tools/checker/match/test.py
+++ b/tools/checker/match/test.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from common.immutables               import ImmutableDict
 from common.testing                  import ToUnicode
 from file_format.c1visualizer.parser import ParseC1visualizerStream
 from file_format.c1visualizer.struct import C1visualizerFile, C1visualizerPass
@@ -33,7 +34,9 @@
     return ParseCheckerAssertion(testCase, checkerString, TestAssertion.Variant.InOrder, 0)
 
   def tryMatch(self, checkerString, c1String, varState={}):
-    return MatchLines(self.createTestAssertion(checkerString), ToUnicode(c1String), varState)
+    return MatchLines(self.createTestAssertion(checkerString),
+                      ToUnicode(c1String),
+                      ImmutableDict(varState))
 
   def matches(self, checkerString, c1String, varState={}):
     return self.tryMatch(checkerString, c1String, varState) is not None