Merge pull request #182 from reventlov/pylint_misc_harmless

Fix miscellaneous harmless lints.
diff --git a/compiler/back_end/cpp/header_generator.py b/compiler/back_end/cpp/header_generator.py
index c80f7ba..3b82db0 100644
--- a/compiler/back_end/cpp/header_generator.py
+++ b/compiler/back_end/cpp/header_generator.py
@@ -187,7 +187,7 @@
 
 
 def _get_namespace_components(namespace):
-    """Gets the components of a C++ namespace
+    """Gets the components of a C++ namespace.
 
     Examples:
       "::some::name::detail" -> ["some", "name", "detail"]
@@ -1338,10 +1338,12 @@
     Arguments:
       type_ir: The IR for the struct definition.
       ir: The full IR; used for type lookups.
+      config: The code generation configuration to use.
 
     Returns:
-      A tuple of: (forward declaration for classes, class bodies, method bodies),
-      suitable for insertion into the appropriate places in the generated header.
+      A tuple of: (forward declaration for classes, class bodies, method
+      bodies), suitable for insertion into the appropriate places in the
+      generated header.
     """
     subtype_bodies, subtype_forward_declarations, subtype_method_definitions = (
         _generate_subtype_definitions(type_ir, ir, config)
@@ -1529,10 +1531,17 @@
 def _split_enum_case_values_into_spans(enum_case_value):
     """Yields spans containing each enum case in an enum_case attribute value.
 
-    Each span is of the form (start, end), which is the start and end position
-    relative to the beginning of the enum_case_value string. To keep the grammar
-    of this attribute simple, this only splits on delimiters and trims whitespace
-    for each case.
+    Arguments:
+        enum_case_value: the value of the `enum_case` attribute to be parsed.
+
+    Returns:
+        An iterator over spans, where each span covers one enum case name.
+        Each span is a half-open range of the form [start, end), which is the
+        start and end position relative to the beginning of the enum_case_value
+        string.  The name can be retrieved with `enum_case_value[start:end]`.
+
+        To keep the grammar of this attribute simple, this only splits on
+        delimiters and trims whitespace for each case.
 
     Example: 'SHOUTY_CASE, kCamelCase' -> [(0, 11), (13, 23)]"""
     # Scan the string from left to right, finding commas and trimming whitespace.
@@ -1567,6 +1576,12 @@
 def _split_enum_case_values(enum_case_value):
     """Returns all enum cases in an enum case value.
 
+    Arguments:
+        enum_case_value: the value of the enum case attribute to parse.
+
+    Returns:
+        All enum case names from `enum_case_value`.
+
     Example: 'SHOUTY_CASE, kCamelCase' -> ['SHOUTY_CASE', 'kCamelCase']"""
     return [
         enum_case_value[start:end]
@@ -1575,7 +1590,7 @@
 
 
 def _get_enum_value_names(enum_value):
-    """Determines one or more enum names based on attributes"""
+    """Determines one or more enum names based on attributes."""
     cases = ["SHOUTY_CASE"]
     name = enum_value.name.name.text
     if enum_case := ir_util.get_attribute(
@@ -1697,14 +1712,16 @@
     Traverses the IR to propagate default values to target nodes.
 
     Arguments:
-      targets: A list of target IR types to add attributes to.
-      ancestors: Ancestor types which may contain the default values.
-      add_fn: Function to add the attribute. May use any parameter available in
-        fast_traverse_ir_top_down actions as well as `defaults` containing the
-        default attributes set by ancestors.
+        ir: The IR to process.
+        targets: A list of target IR types to add attributes to.
+        ancestors: Ancestor types which may contain the default values.
+        add_fn: Function to add the attribute. May use any parameter available
+            in fast_traverse_ir_top_down actions as well as `defaults`
+            containing the
+            default attributes set by ancestors.
 
     Returns:
-      None
+        None
     """
     traverse_ir.fast_traverse_ir_top_down(
         ir,
@@ -1718,14 +1735,19 @@
 
 
 def _offset_source_location_column(source_location, offset):
-    """Adds offsets from the start column of the supplied source location
+    """Adds offsets from the start column of the supplied source location.
 
-    Returns a new source location with all of the same properties as the provided
-    source location, but with the columns modified by offsets from the original
-    start column.
+    Arguments:
+        source_location: the initial source location
+        offset: a tuple of (start, end), which are the offsets relative to
+            source_location.start.column to set the new start.column and
+            end.column.
 
-    Offset should be a tuple of (start, end), which are the offsets relative to
-    source_location.start.column to set the new start.column and end.column."""
+    Returns:
+        A new source location with all of the same properties as the provided
+        source location, but with the columns modified by offsets from the
+        original start column.
+    """
 
     new_location = ir_data_utils.copy(source_location)
     new_location.start.column = source_location.start.column + offset[0]
@@ -1738,8 +1760,8 @@
     if attr.name.text != attributes.Attribute.NAMESPACE:
         return
     namespace_value = ir_data_utils.reader(attr).value.string_constant
-    if not re.match(_NS_RE, namespace_value.text):
-        if re.match(_NS_EMPTY_RE, namespace_value.text):
+    if not re.fullmatch(_NS_RE, namespace_value.text):
+        if re.fullmatch(_NS_EMPTY_RE, namespace_value.text):
             errors.append(
                 [
                     error.error(
@@ -1749,7 +1771,7 @@
                     )
                 ]
             )
-        elif re.match(_NS_GLOBAL_RE, namespace_value.text):
+        elif re.fullmatch(_NS_GLOBAL_RE, namespace_value.text):
             errors.append(
                 [
                     error.error(
@@ -1864,8 +1886,12 @@
 def _propagate_defaults_and_verify_attributes(ir):
     """Verify attributes and ensure defaults are set when not overridden.
 
-    Returns a list of errors if there are errors present, or an empty list if
-    verification completed successfully."""
+    Arguments:
+        ir: The IR to process.
+
+    Returns:
+        A list of errors if there are errors present, or an empty list if
+        verification completed successfully."""
     if errors := attribute_util.check_attributes_in_ir(
         ir,
         back_end="cpp",
diff --git a/compiler/back_end/util/code_template_test.py b/compiler/back_end/util/code_template_test.py
index e4354fe..8149aaa 100644
--- a/compiler/back_end/util/code_template_test.py
+++ b/compiler/back_end/util/code_template_test.py
@@ -55,7 +55,7 @@
     """Tests for code_template.parse_templates."""
 
     def assertTemplatesEqual(self, expected, actual):  # pylint:disable=invalid-name
-        """Compares the results of a parse_templates"""
+        """Compares the results of a parse_templates."""
         # Extract the name and template from the result tuple
         actual = {k: v.template for k, v in actual._asdict().items()}
         self.assertEqual(expected, actual)
diff --git a/compiler/front_end/attribute_checker.py b/compiler/front_end/attribute_checker.py
index 79c1495..232eee8 100644
--- a/compiler/front_end/attribute_checker.py
+++ b/compiler/front_end/attribute_checker.py
@@ -44,8 +44,9 @@
 
 
 def _valid_back_ends(attr, module_source_file):
-    if not re.match(
-        r"^(?:\s*[a-z][a-z0-9_]*\s*(?:,\s*[a-z][a-z0-9_]*\s*)*,?)?\s*$",
+    """Checks that `attr` holds a valid list of back end specifiers."""
+    if not re.fullmatch(
+        r"(?:\s*[a-z][a-z0-9_]*\s*(?:,\s*[a-z][a-z0-9_]*\s*)*,?)?\s*",
         attr.value.string_constant.text,
     ):
         return [
diff --git a/compiler/front_end/constraints.py b/compiler/front_end/constraints.py
index 3249e6d..852c9ad 100644
--- a/compiler/front_end/constraints.py
+++ b/compiler/front_end/constraints.py
@@ -436,6 +436,7 @@
 
 
 def _check_allowed_in_bits(type_ir, type_definition, source_file_name, ir, errors):
+    """Verifies that atomic fields have types that are allowed in `bits`."""
     if not type_ir.HasField("atomic_type"):
         return
     referenced_type_definition = ir_util.find_object(type_ir.atomic_type.reference, ir)
diff --git a/compiler/front_end/dependency_checker.py b/compiler/front_end/dependency_checker.py
index 8a9e903..fb622e1 100644
--- a/compiler/front_end/dependency_checker.py
+++ b/compiler/front_end/dependency_checker.py
@@ -23,14 +23,15 @@
 def _add_reference_to_dependencies(
     reference, dependencies, name, source_file_name, errors
 ):
+    """Adds the specified `reference` to the `dependencies` set."""
     if reference.canonical_name.object_path[0] in {
         "$is_statically_sized",
         "$static_size_in_bits",
         "$next",
     }:
-        # This error is a bit opaque, but given that the compiler used to crash on
-        # this case -- for a couple of years -- and no one complained, it seems
-        # safe to assume that this is a rare error.
+        # This error is a bit opaque, but given that the compiler used to crash
+        # on this case -- for a couple of years -- and no one complained, it
+        # seems safe to assume that this is a rare error.
         errors.append(
             [
                 error.error(
diff --git a/compiler/front_end/docs_are_up_to_date_test.py b/compiler/front_end/docs_are_up_to_date_test.py
index 00ab32a..e558bea 100644
--- a/compiler/front_end/docs_are_up_to_date_test.py
+++ b/compiler/front_end/docs_are_up_to_date_test.py
@@ -26,16 +26,12 @@
     def test_grammar_md(self):
         doc_md = pkgutil.get_data("doc", "grammar.md").decode(encoding="UTF-8")
         correct_md = generate_grammar_md.generate_grammar_md()
-        # If this fails, run:
-        #
-        # bazel run //compiler/front_end:generate_grammar_md > doc/grammar.md
-        #
-        # Be sure to check that the results look good before committing!
+        msg = "Run:\n\nbazel run //compiler/front_end:generate_grammar_md > doc/grammar.md"
         doc_md_lines = doc_md.splitlines()
         correct_md_lines = correct_md.splitlines()
         for i in range(len(doc_md_lines)):
-            self.assertEqual(correct_md_lines[i], doc_md_lines[i])
-        self.assertEqual(correct_md, doc_md)
+            self.assertEqual(correct_md_lines[i], doc_md_lines[i], msg=msg)
+        self.assertEqual(correct_md, doc_md, msg=msg)
 
 
 if __name__ == "__main__":
diff --git a/compiler/front_end/emboss_front_end.py b/compiler/front_end/emboss_front_end.py
index c62638d..269cc34 100644
--- a/compiler/front_end/emboss_front_end.py
+++ b/compiler/front_end/emboss_front_end.py
@@ -56,6 +56,11 @@
         "before symbol resolution.",
     )
     parser.add_argument(
+        "--debug-stop-before-step",
+        type=str,
+        help="Stop processing before the specified step.",
+    )
+    parser.add_argument(
         "--debug-show-full-ir",
         action="store_true",
         help="Show the final IR of the main input file.",
@@ -146,7 +151,7 @@
     return _find_and_read
 
 
-def parse_and_log_errors(input_file, import_dirs, color_output):
+def parse_and_log_errors(input_file, import_dirs, color_output, stop_before_step=None):
     """Fully parses an .emb and logs any errors.
 
     Arguments:
@@ -158,7 +163,9 @@
       (ir, debug_info, errors)
     """
     ir, debug_info, errors = glue.parse_emboss_file(
-        input_file, _find_in_dirs_and_read(import_dirs)
+        input_file,
+        _find_in_dirs_and_read(import_dirs),
+        stop_before_step=stop_before_step,
     )
     if errors:
         _show_errors(errors, ir, color_output)
@@ -168,7 +175,10 @@
 
 def main(flags):
     ir, debug_info, errors = parse_and_log_errors(
-        flags.input_file[0], flags.import_dirs, flags.color_output
+        flags.input_file[0],
+        flags.import_dirs,
+        flags.color_output,
+        stop_before_step=flags.debug_stop_before_step,
     )
     if errors:
         return 1
diff --git a/compiler/front_end/format_emb.py b/compiler/front_end/format_emb.py
index df9bcf3..fc7bf94 100644
--- a/compiler/front_end/format_emb.py
+++ b/compiler/front_end/format_emb.py
@@ -485,6 +485,7 @@
     "               type-definition* struct-field-block Dedent"
 )
 def _structure_body(indent, docs, attributes, type_definitions, fields, dedent, config):
+    """Formats a structure (`bits` or `struct`) body."""
     del indent, dedent  # Unused.
     spacing = [_Row("field-separator")] if _should_add_blank_lines(fields) else []
     columnized_fields = _columnize(fields, config.indent_width, indent_columns=2)
@@ -609,6 +610,7 @@
     '    field-location "bits" ":" Comment? eol anonymous-bits-body'
 )
 def _inline_bits(location, bits, colon, comment, eol, body):
+    """Formats an inline `bits` definition."""
     # Even though an anonymous bits field technically defines a new, anonymous
     # type, conceptually it's more like defining a bunch of fields on the
     # surrounding type, so it is treated as an inline list of blocks, instead of
@@ -1017,6 +1019,7 @@
 @_formats("or-expression-right -> or-operator comparison-expression")
 @_formats("and-expression-right -> and-operator comparison-expression")
 def _concatenate_with_prefix_spaces(*elements):
+    """Concatenates non-empty `elements` with leading spaces."""
     return "".join(" " + element for element in elements if element)
 
 
@@ -1032,10 +1035,12 @@
 )
 @_formats('parameter-definition-list-tail -> "," parameter-definition')
 def _concatenate_with_spaces(*elements):
+    """Concatenates non-empty `elements` with spaces between."""
     return _concatenate_with(" ", *elements)
 
 
 def _concatenate_with(joiner, *elements):
+    """Concatenates non-empty `elements` with `joiner` between."""
     return joiner.join(element for element in elements if element)
 
 
diff --git a/compiler/front_end/glue.py b/compiler/front_end/glue.py
index 2744086..f12f27e 100644
--- a/compiler/front_end/glue.py
+++ b/compiler/front_end/glue.py
@@ -19,6 +19,7 @@
 """
 
 import collections
+import sys
 
 from compiler.front_end import attribute_checker
 from compiler.front_end import constraints
@@ -323,9 +324,11 @@
         constraints.check_constraints,
         write_inference.set_write_methods,
     )
-    assert stop_before_step in [None] + [
-        f.__name__ for f in passes
-    ], "Bad value for stop_before_step."
+    valid_step_names = [f.__name__ for f in passes]
+    assert stop_before_step in [None] + valid_step_names, (
+        f"Bad value '{stop_before_step}' for stop_before_step.  Valid values: "
+        + " ".join(valid_step_names)
+    )
     # Some parts of the IR are synthesized from "natural" parts of the IR, before
     # the natural parts have been fully error checked.  Because of this, the
     # synthesized parts can have errors; in a couple of cases, they can have
diff --git a/compiler/front_end/lr1.py b/compiler/front_end/lr1.py
index be99f95..610820a 100644
--- a/compiler/front_end/lr1.py
+++ b/compiler/front_end/lr1.py
@@ -36,16 +36,17 @@
 ):
     """An Item is an LR(1) Item: a production, a cursor location, and a terminal.
 
-    An Item represents a partially-parsed production, and a lookahead symbol.  The
-    position of the dot indicates what portion of the production has been parsed.
-    Generally, Items are an internal implementation detail, but they can be useful
-    elsewhere, particularly for debugging.
+    An Item represents a partially-parsed production, and a lookahead symbol.
+    The position of the dot indicates what portion of the production has been
+    parsed.  Generally, Items are an internal implementation detail, but they
+    can be useful elsewhere, particularly for debugging.
 
     Attributes:
-      production: The Production this Item covers.
-      dot: The index of the "dot" in production's rhs.
-      terminal: The terminal lookahead symbol that follows the production in the
-          input stream.
+        production: The Production this Item covers.
+        dot: The index of the "dot" in production's rhs.
+        terminal: The terminal lookahead symbol that follows the production in
+            the input stream.
+        next_symbol: The lookahead symbol.
     """
 
     def __str__(self):
@@ -197,11 +198,9 @@
     def _set_productions_by_lhs(self):
         # Prepopulating _productions_by_lhs speeds up _closure_of_item by about 30%,
         # which is significant on medium-to-large grammars.
-        self._productions_by_lhs = {}
+        self._productions_by_lhs = collections.defaultdict(list)
         for production in self.productions:
-            self._productions_by_lhs.setdefault(production.lhs, list()).append(
-                production
-            )
+            self._productions_by_lhs[production.lhs].append(production)
 
     def _populate_item_cache(self):
         # There are a relatively small number of possible Items for a grammar, and
@@ -455,7 +454,7 @@
             )
         ]
         items = {item_list[0]: 0}
-        goto_table = {}
+        goto_table = collections.defaultdict(dict)
         i = 0
         # For each state, figure out what the new state when each symbol is added to
         # the top of the parsing stack (see the comments in parser._parse).  See
@@ -468,7 +467,7 @@
                 if goto not in items:
                     items[goto] = len(item_list)
                     item_list.append(goto)
-                goto_table[i, symbol] = items[goto]
+                goto_table[i][symbol] = items[goto]
             i += 1
         return item_list, goto_table
 
@@ -493,7 +492,7 @@
           A Parser.
         """
         item_sets, goto = self._items()
-        action = {}
+        action = collections.defaultdict(dict)
         conflicts = set()
         end_item = self._item_cache[self._seed_production, 1, END_OF_INPUT]
         for i in range(len(item_sets)):
@@ -507,38 +506,31 @@
                     new_action = Reduce(item.production)
                 elif item.next_symbol in self.terminals:
                     terminal = item.next_symbol
-                    assert goto[i, terminal] is not None
-                    new_action = Shift(goto[i, terminal], item_sets[goto[i, terminal]])
+                    assert goto[i][terminal] is not None
+                    new_action = Shift(goto[i][terminal], item_sets[goto[i][terminal]])
                 if new_action:
-                    if (i, terminal) in action and action[i, terminal] != new_action:
+                    if action[i].get(terminal, new_action) != new_action:
                         conflicts.add(
                             Conflict(
                                 i,
                                 terminal,
-                                frozenset([action[i, terminal], new_action]),
+                                frozenset([action[i][terminal], new_action]),
                             )
                         )
-                    action[i, terminal] = new_action
+                    action[i][terminal] = new_action
                 if item == end_item:
                     new_action = Accept()
-                    assert (i, END_OF_INPUT) not in action or action[
-                        i, END_OF_INPUT
-                    ] == new_action
-                    action[i, END_OF_INPUT] = new_action
-        trimmed_goto = {}
+                    assert action[i].get(END_OF_INPUT, new_action) == new_action
+                    action[i][END_OF_INPUT] = new_action
+        trimmed_goto = collections.defaultdict(dict)
         for k in goto:
-            if k[1] in self.nonterminals:
-                trimmed_goto[k] = goto[k]
-        expected = {}
-        for state, terminal in action:
-            if state not in expected:
-                expected[state] = set()
-            expected[state].add(terminal)
+            for l in goto[k]:
+                if l in self.nonterminals:
+                    trimmed_goto[k][l] = goto[k][l]
         return Parser(
             item_sets,
             trimmed_goto,
             action,
-            expected,
             conflicts,
             self.terminals,
             self.nonterminals,
@@ -584,7 +576,6 @@
         item_sets,
         goto,
         action,
-        expected,
         conflicts,
         terminals,
         nonterminals,
@@ -594,7 +585,6 @@
         self.item_sets = item_sets
         self.goto = goto
         self.action = action
-        self.expected = expected
         self.conflicts = conflicts
         self.terminals = terminals
         self.nonterminals = nonterminals
@@ -633,7 +623,7 @@
         # On each iteration, look at the next symbol and the current state, and
         # perform the corresponding action.
         while True:
-            if (state(), tokens[cursor].symbol) not in self.action:
+            if tokens[cursor].symbol not in self.action.get(state(), {}):
                 # Most state/symbol entries would be Errors, so rather than exhaustively
                 # adding error entries, we just check here.
                 if state() in self.default_errors:
@@ -641,7 +631,7 @@
                 else:
                     next_action = Error(None)
             else:
-                next_action = self.action[state(), tokens[cursor].symbol]
+                next_action = self.action[state()][tokens[cursor].symbol]
 
             if isinstance(next_action, Shift):
                 # Shift means that there are no "complete" productions on the stack,
@@ -716,7 +706,7 @@
                     next_action.rule.lhs, children, next_action.rule, source_location
                 )
                 del stack[len(stack) - len(next_action.rule.rhs) :]
-                stack.append((self.goto[state(), next_action.rule.lhs], reduction))
+                stack.append((self.goto[state()][next_action.rule.lhs], reduction))
             elif isinstance(next_action, Error):
                 # Error means that the parse is impossible.  For typical grammars and
                 # texts, this usually happens within a few tokens after the mistake in
@@ -729,7 +719,11 @@
                         cursor,
                         tokens[cursor],
                         state(),
-                        self.expected[state()],
+                        set(
+                            k
+                            for k in self.action[state()].keys()
+                            if not isinstance(self.action[state()][k], Error)
+                        ),
                     ),
                 )
             else:
@@ -800,8 +794,8 @@
                 self.default_errors[result.error.state] = error_code
                 return None
         else:
-            if (result.error.state, error_symbol) in self.action:
-                existing_error = self.action[result.error.state, error_symbol]
+            if error_symbol in self.action.get(result.error.state, {}):
+                existing_error = self.action[result.error.state][error_symbol]
                 assert isinstance(existing_error, Error), "Bug"
                 if existing_error.code == error_code:
                     return None
@@ -816,7 +810,7 @@
                         )
                     )
             else:
-                self.action[result.error.state, error_symbol] = Error(error_code)
+                self.action[result.error.state][error_symbol] = Error(error_code)
                 return None
         assert False, "All other paths should lead to return."
 
@@ -829,5 +823,4 @@
         Returns:
           A ParseResult.
         """
-        result = self._parse(tokens)
-        return result
+        return self._parse(tokens)
diff --git a/compiler/front_end/lr1_test.py b/compiler/front_end/lr1_test.py
index ae03e2d..6ca6e67 100644
--- a/compiler/front_end/lr1_test.py
+++ b/compiler/front_end/lr1_test.py
@@ -98,31 +98,42 @@
 
 # ACTION table corresponding to the above grammar, ASLU p266.
 _alsu_action = {
-    (0, "c"): lr1.Shift(3, _alsu_items[3]),
-    (0, "d"): lr1.Shift(4, _alsu_items[4]),
-    (1, lr1.END_OF_INPUT): lr1.Accept(),
-    (2, "c"): lr1.Shift(6, _alsu_items[6]),
-    (2, "d"): lr1.Shift(7, _alsu_items[7]),
-    (3, "c"): lr1.Shift(3, _alsu_items[3]),
-    (3, "d"): lr1.Shift(4, _alsu_items[4]),
-    (4, "c"): lr1.Reduce(parser_types.Production("C", ("d",))),
-    (4, "d"): lr1.Reduce(parser_types.Production("C", ("d",))),
-    (5, lr1.END_OF_INPUT): lr1.Reduce(parser_types.Production("S", ("C", "C"))),
-    (6, "c"): lr1.Shift(6, _alsu_items[6]),
-    (6, "d"): lr1.Shift(7, _alsu_items[7]),
-    (7, lr1.END_OF_INPUT): lr1.Reduce(parser_types.Production("C", ("d",))),
-    (8, "c"): lr1.Reduce(parser_types.Production("C", ("c", "C"))),
-    (8, "d"): lr1.Reduce(parser_types.Production("C", ("c", "C"))),
-    (9, lr1.END_OF_INPUT): lr1.Reduce(parser_types.Production("C", ("c", "C"))),
+    0: {
+        "c": lr1.Shift(3, _alsu_items[3]),
+        "d": lr1.Shift(4, _alsu_items[4]),
+    },
+    1: {lr1.END_OF_INPUT: lr1.Accept()},
+    2: {
+        "c": lr1.Shift(6, _alsu_items[6]),
+        "d": lr1.Shift(7, _alsu_items[7]),
+    },
+    3: {
+        "c": lr1.Shift(3, _alsu_items[3]),
+        "d": lr1.Shift(4, _alsu_items[4]),
+    },
+    4: {
+        "c": lr1.Reduce(parser_types.Production("C", ("d",))),
+        "d": lr1.Reduce(parser_types.Production("C", ("d",))),
+    },
+    5: {lr1.END_OF_INPUT: lr1.Reduce(parser_types.Production("S", ("C", "C")))},
+    6: {
+        "c": lr1.Shift(6, _alsu_items[6]),
+        "d": lr1.Shift(7, _alsu_items[7]),
+    },
+    7: {lr1.END_OF_INPUT: lr1.Reduce(parser_types.Production("C", ("d",)))},
+    8: {
+        "c": lr1.Reduce(parser_types.Production("C", ("c", "C"))),
+        "d": lr1.Reduce(parser_types.Production("C", ("c", "C"))),
+    },
+    9: {lr1.END_OF_INPUT: lr1.Reduce(parser_types.Production("C", ("c", "C")))},
 }
 
 # GOTO table corresponding to the above grammar, ASLU p266.
 _alsu_goto = {
-    (0, "S"): 1,
-    (0, "C"): 2,
-    (2, "C"): 5,
-    (3, "C"): 8,
-    (6, "C"): 9,
+    0: {"S": 1, "C": 2},
+    2: {"C": 5},
+    3: {"C": 8},
+    6: {"C": 9},
 }
 
 
@@ -137,15 +148,17 @@
         original_index_to_index[item_to_original_index[sorted_items[i]]] = i
     updated_table = {}
     for k in table:
-        new_k = original_index_to_index[k[0]], k[1]
-        new_value = table[k]
-        if isinstance(new_value, int):
-            new_value = original_index_to_index[new_value]
-        elif isinstance(new_value, lr1.Shift):
-            new_value = lr1.Shift(
-                original_index_to_index[new_value.state], new_value.items
-            )
-        updated_table[new_k] = new_value
+        for l in table[k]:
+            new_k = original_index_to_index[k]
+            new_value = table[k][l]
+            if isinstance(new_value, int):
+                new_value = original_index_to_index[new_value]
+            elif isinstance(new_value, lr1.Shift):
+                new_value = lr1.Shift(
+                    original_index_to_index[new_value.state], new_value.items
+                )
+            updated_table.setdefault(new_k, {})
+            updated_table[new_k][l] = new_value
     return sorted_items, updated_table
 
 
@@ -302,7 +315,7 @@
         # Marking an already-marked error with the same error code should succeed.
         self.assertIsNone(parser.mark_error(_tokenize("d"), None, "missing last C"))
         # Marking an already-marked error with a different error code should fail.
-        self.assertRegexpMatches(
+        self.assertRegex(
             parser.mark_error(_tokenize("d"), None, "different message"),
             r"^Attempted to overwrite existing error code 'missing last C' with "
             r"new error code 'different message' for state \d+, terminal \$$",
diff --git a/compiler/front_end/module_ir.py b/compiler/front_end/module_ir.py
index bd27c8a..4a459c2 100644
--- a/compiler/front_end/module_ir.py
+++ b/compiler/front_end/module_ir.py
@@ -339,6 +339,7 @@
     attribute_value,
     close_bracket,
 ):
+    """Assembles an attribute IR node."""
     del open_bracket, colon, close_bracket  # Unused.
     if context_specifier.list:
         return ir_data.Attribute(
@@ -460,14 +461,15 @@
     '                                        ":" logical-expression'
 )
 def _choice_expression(condition, question, if_true, colon, if_false):
+    """Constructs an IR node for a choice operator (`?:`) expression."""
     location = parser_types.make_location(
         condition.source_location.start, if_false.source_location.end
     )
     operator_location = parser_types.make_location(
         question.source_location.start, colon.source_location.end
     )
-    # The function_name is a bit weird, but should suffice for any error messages
-    # that might need it.
+    # The function_name is a bit weird, but should suffice for any error
+    # messages that might need it.
     return ir_data.Expression(
         function=ir_data.Function(
             function=ir_data.FunctionMapping.CHOICE,
@@ -1284,6 +1286,7 @@
 def _enum_value(
     name, equals, expression, attribute, documentation, comment, newline, body
 ):
+    """Constructs an IR node for an enum value statement (`NAME = value`)."""
     del equals, comment, newline  # Unused.
     result = ir_data.EnumValue(
         name=name,
diff --git a/compiler/front_end/synthetics.py b/compiler/front_end/synthetics.py
index 1b331a3..f55e32f 100644
--- a/compiler/front_end/synthetics.py
+++ b/compiler/front_end/synthetics.py
@@ -236,6 +236,7 @@
 def _maybe_replace_next_keyword_in_expression(
     expression_ir, last_location, source_file_name, errors
 ):
+    """Replaces the `$next` keyword in an expression."""
     if not expression_ir.HasField("builtin_reference"):
         return
     if (
diff --git a/compiler/front_end/type_check.py b/compiler/front_end/type_check.py
index f562cc8..c3f7870 100644
--- a/compiler/front_end/type_check.py
+++ b/compiler/front_end/type_check.py
@@ -133,6 +133,7 @@
 
 
 def _type_check_operation(expression, source_file_name, ir, errors):
+    """Type checks a function or operator expression."""
     for arg in expression.function.args:
         _type_check_expression(arg, source_file_name, ir, errors)
     function = expression.function.function
diff --git a/compiler/util/attribute_util.py b/compiler/util/attribute_util.py
index a83cf51..2ebbffa 100644
--- a/compiler/util/attribute_util.py
+++ b/compiler/util/attribute_util.py
@@ -323,22 +323,24 @@
 ):
     """Performs basic checks on the given list of attributes.
 
-    Checks the given attribute_list for duplicates, unknown attributes, attributes
-    with incorrect type, and attributes whose values are not constant.
+    Checks the given attribute_list for duplicates, unknown attributes,
+    attributes with incorrect type, and attributes whose values are not
+    constant.
 
     Arguments:
-      attribute_list: An iterable of ir_data.Attribute.
-      back_end: The qualifier for attributes to check, or None.
-      attribute_specs: A dict of attribute names to _Attribute structures
-        specifying the allowed attributes.
-      context_name: A name for the context of these attributes, such as "struct
-        'Foo'" or "module 'm.emb'".  Used in error messages.
-      module_source_file: The value of module.source_file_name from the module
-        containing 'attribute_list'.  Used in error messages.
+        attribute_list: An iterable of ir_data.Attribute.
+        types: A map of attribute types to validators.
+        back_end: The qualifier for attributes to check, or None.
+        attribute_specs: A dict of attribute names to _Attribute structures
+            specifying the allowed attributes.
+        context_name: A name for the context of these attributes, such as
+            "struct 'Foo'" or "module 'm.emb'".  Used in error messages.
+        module_source_file: The value of module.source_file_name from the module
+            containing 'attribute_list'.  Used in error messages.
 
     Returns:
-      A list of lists of error.Errors.  An empty list indicates no errors were
-      found.
+        A list of lists of error.Errors.  An empty list indicates no errors were
+        found.
     """
     if attribute_specs is None:
         attribute_specs = []
@@ -395,17 +397,17 @@
 
 
 def gather_default_attributes(obj, defaults):
-    """Gathers default attributes for an IR object
+    """Gathers default attributes for an IR object.
 
-    This is designed to be able to be used as-is as an incidental action in an IR
-    traversal to accumulate defaults for child nodes.
+    This is designed to be able to be used as-is as an incidental action in an
+    IR traversal to accumulate defaults for child nodes.
 
     Arguments:
-      defaults: A dict of `{ "defaults": { attr.name.text: attr } }`
+        defaults: A dict of `{ "defaults": { attr.name.text: attr } }`
 
     Returns:
-      A dict of `{ "defaults": { attr.name.text: attr } }` with any defaults
-      provided by `obj` added/overridden.
+        A dict of `{ "defaults": { attr.name.text: attr } }` with any defaults
+        provided by `obj` added/overridden.
     """
     defaults = defaults.copy()
     for attr in obj.attribute:
diff --git a/compiler/util/ir_data.py b/compiler/util/ir_data.py
index af8c2f7..fda124e 100644
--- a/compiler/util/ir_data.py
+++ b/compiler/util/ir_data.py
@@ -110,7 +110,10 @@
     def WhichOneof(self, oneof_name):  # pylint:disable=invalid-name
         """Indicates which field has been set for the oneof value.
 
-        Returns None if no field has been set.
+        Args:
+            oneof_name: the name of the oneof construct to test.
+
+        Returns: the field name, or None if no field has been set.
         """
         for field_name, oneof in self.field_specs.oneof_mappings:
             if oneof == oneof_name and self.HasField(field_name):
@@ -207,7 +210,7 @@
 
 
 class FunctionMapping(int, enum.Enum):
-    """Enum of supported function types"""
+    """Enum of supported function types."""
 
     UNKNOWN = 0
     ADDITION = 1
@@ -823,8 +826,9 @@
 
 
 class AddressableUnit(int, enum.Enum):
-    """The "addressable unit" is the size of the smallest unit that can be read
+    """The 'atom size' for a structure.
 
+    The "addressable unit" is the size of the smallest unit that can be read
     from the backing store that this type expects.  For `struct`s, this is
     BYTE; for `enum`s and `bits`, this is BIT, and for `external`s it depends
     on the specific type
diff --git a/compiler/util/ir_data_fields.py b/compiler/util/ir_data_fields.py
index 002ea3b..76df36c 100644
--- a/compiler/util/ir_data_fields.py
+++ b/compiler/util/ir_data_fields.py
@@ -76,10 +76,10 @@
 
 
 class CopyValuesList(list[CopyValuesListT]):
-    """A list that makes copies of any value that is inserted"""
+    """A list that makes copies of any value that is inserted."""
 
     def __init__(
-        self, value_type: CopyValuesListT, iterable: Optional[Iterable] = None
+        self, value_type: CopyValuesListT, iterable: Optional[Iterable[Any]] = None
     ):
         if iterable:
             super().__init__(iterable)
@@ -96,7 +96,7 @@
         return super().extend([self._copy(i) for i in iterable])
 
     def shallow_copy(self, iterable: Iterable) -> None:
-        """Explicitly performs a shallow copy of the provided list"""
+        """Explicitly performs a shallow copy of the provided list."""
         return super().extend(iterable)
 
     def append(self, obj: Any) -> None:
@@ -107,15 +107,13 @@
 
 
 class TemporaryCopyValuesList(NamedTuple):
-    """Class used to temporarily hold a CopyValuesList while copying and
-    constructing an IR dataclass.
-    """
+    """Holder for a CopyValuesList while copying/constructing an IR dataclass."""
 
     temp_list: CopyValuesList
 
 
 class FieldContainer(enum.Enum):
-    """Indicates a fields container type"""
+    """Indicates a fields container type."""
 
     NONE = 0
     OPTIONAL = 1
@@ -125,8 +123,8 @@
 class FieldSpec(NamedTuple):
     """Indicates the container and type of a field.
 
-    `FieldSpec` objects are accessed millions of times during runs so we cache as
-    many operations as possible.
+    `FieldSpec` objects are accessed millions of times during runs so we cache
+    as many operations as possible.
       - `is_dataclass`: `dataclasses.is_dataclass(data_type)`
       - `is_sequence`: `container is FieldContainer.LIST`
       - `is_enum`: `issubclass(data_type, enum.Enum)`
@@ -162,7 +160,7 @@
 
 
 def build_default(field_spec: FieldSpec):
-    """Builds a default instance of the given field"""
+    """Builds a default instance of the given field."""
     if field_spec.is_sequence:
         return CopyValuesList(field_spec.data_type)
     if field_spec.is_enum:
@@ -216,11 +214,20 @@
 
 
 def cache_message_specs(mod, cls):
-    """Adds a cached `field_specs` attribute to IR dataclasses in `mod`
-    excluding the given base `cls`.
+    """Adds `field_specs` to `mod`, excluding `cls`.
+
+    Adds a cached `field_specs` attribute to IR dataclasses in module `mod`
+    excluding the given base class `cls`.
 
     This needs to be done after the dataclass decorators run and create the
     wrapped classes.
+
+    Arguments:
+        mod: The module to process.
+        cls: The base class to exclude.
+
+    Returns:
+        None
     """
     for data_class in all_ir_classes(mod):
         if data_class is not cls:
@@ -228,13 +235,13 @@
 
 
 def _field_specs(cls: type[IrDataT]) -> Mapping[str, FieldSpec]:
-    """Gets the IR data field names and types for the given IR data class"""
+    """Gets the IR data field names and types for the given IR data class."""
     # Get the dataclass fields
     class_fields = dataclasses.fields(cast(Any, cls))
 
     # Pre-python 3.11 (maybe pre 3.10) `get_type_hints` will substitute
     # `builtins.Expression` for 'Expression' rather than `ir_data.Expression`.
-    # Instead we manually subsitute the type by extracting the list of classes
+    # Instead we manually substitute the type by extracting the list of classes
     # from the class' module and manually substituting.
     mod_ns = {
         k: v
@@ -284,9 +291,15 @@
 
 
 def field_specs(obj: Union[IrDataT, type[IrDataT]]) -> Mapping[str, FieldSpec]:
-    """Retrieves the fields specs for the the give data type.
+    """Retrieves the fields specs for the give data type.
 
     The results of this method are cached to reduce lookup overhead.
+
+    Arguments:
+        obj: Either an IR dataclass type, or an instance of such a type.
+
+    Returns:
+        The field specs for `obj`.
     """
     cls = obj if isinstance(obj, type) else type(obj)
     if cls is type(None):
@@ -301,8 +314,11 @@
     """Retrieves the fields and their values for a given IR data class.
 
     Args:
-      ir: The IR data class or a read-only wrapper of an IR data class.
-      value_filt: Optional filter used to exclude values.
+        ir: The IR data class or a read-only wrapper of an IR data class.
+        value_filt: Optional filter used to exclude values.
+
+    Returns:
+        None
     """
     set_fields: list[Tuple[FieldSpec, Any]] = []
     specs: FilteredIrFieldSpecs = ir.field_specs
@@ -323,12 +339,13 @@
 #    `all_field_specs` dict.
 # 3. Copied lists are wrapped in a `TemporaryCopyValuesList`. This is used to
 #    signal to consumers that they can take ownership of the contained list
-#    rather than copying it again. See `ir_data.Message()` and `udpate()` for
+#    rather than copying it again. See `ir_data.Message()` and `update()` for
 #    where this is used.
 # 4. `FieldSpec` checks are cached including `is_dataclass` and `is_sequence`.
 # 5. None checks are only done in `copy()`, `_copy_set_fields` only
 #    references `_copy()` to avoid this step.
 def _copy_set_fields(ir: IrDataT):
+    """Deep copies fields from IR node `ir`."""
     values: MutableMapping[str, Any] = {}
 
     specs: FilteredIrFieldSpecs = ir.field_specs
@@ -355,7 +372,7 @@
 
 
 def copy(ir: IrDataT) -> Optional[IrDataT]:
-    """Creates a copy of the given IR data class"""
+    """Creates a copy of the given IR data class."""
     if not ir:
         return None
     return _copy(ir)
@@ -413,14 +430,14 @@
 
 
 def oneof_field(name: str):
-    """Alternative for `datclasses.field` that sets up a oneof variable"""
+    """Alternative for `datclasses.field` that sets up a oneof variable."""
     return dataclasses.field(  # pylint:disable=invalid-field-call
         default=OneOfField(name), metadata={"oneof": name}, init=True
     )
 
 
 def str_field():
-    """Helper used to define a defaulted str field"""
+    """Helper used to define a defaulted str field."""
     return dataclasses.field(default_factory=str)  # pylint:disable=invalid-field-call
 
 
@@ -436,7 +453,10 @@
     ```
 
     Args:
-      cls_or_fn: The class type or a function that resolves to the class type.
+        cls_or_fn: The class type or a function that resolves to the class type.
+
+    Returns:
+        A field with a `default_factory` that produces an appropriate list.
     """
 
     def list_factory(c):
diff --git a/compiler/util/ir_data_fields_test.py b/compiler/util/ir_data_fields_test.py
index 344dd13..f6ee2ca 100644
--- a/compiler/util/ir_data_fields_test.py
+++ b/compiler/util/ir_data_fields_test.py
@@ -34,12 +34,12 @@
 
 @dataclasses.dataclass
 class Opaque(ir_data.Message):
-    """Used for testing data field helpers"""
+    """Used for testing data field helpers."""
 
 
 @dataclasses.dataclass
 class ClassWithUnion(ir_data.Message):
-    """Used for testing data field helpers"""
+    """Used for testing data field helpers."""
 
     opaque: Optional[Opaque] = ir_data_fields.oneof_field("type")
     integer: Optional[int] = ir_data_fields.oneof_field("type")
@@ -50,7 +50,7 @@
 
 @dataclasses.dataclass
 class ClassWithTwoUnions(ir_data.Message):
-    """Used for testing data field helpers"""
+    """Used for testing data field helpers."""
 
     opaque: Optional[Opaque] = ir_data_fields.oneof_field("type_1")
     integer: Optional[int] = ir_data_fields.oneof_field("type_1")
@@ -62,7 +62,7 @@
 
 @dataclasses.dataclass
 class NestedClass(ir_data.Message):
-    """Used for testing data field helpers"""
+    """Used for testing data field helpers."""
 
     one_union_class: Optional[ClassWithUnion] = None
     two_union_class: Optional[ClassWithTwoUnions] = None
@@ -78,7 +78,7 @@
 
 @dataclasses.dataclass
 class OneofFieldTest(ir_data.Message):
-    """Basic test class for oneof fields"""
+    """Basic test class for oneof fields."""
 
     int_field_1: Optional[int] = ir_data_fields.oneof_field("type_1")
     int_field_2: Optional[int] = ir_data_fields.oneof_field("type_1")
@@ -86,7 +86,7 @@
 
 
 class OneOfTest(unittest.TestCase):
-    """Tests for the the various oneof field helpers"""
+    """Tests for the various oneof field helpers."""
 
     def test_field_attribute(self):
         """Test the `oneof_field` helper."""
@@ -97,21 +97,21 @@
         self.assertEqual(test_field.metadata.get("oneof"), "type_1")
 
     def test_init_default(self):
-        """Test creating an instance with default fields"""
+        """Test creating an instance with default fields."""
         one_of_field_test = OneofFieldTest()
         self.assertIsNone(one_of_field_test.int_field_1)
         self.assertIsNone(one_of_field_test.int_field_2)
         self.assertTrue(one_of_field_test.normal_field)
 
     def test_init(self):
-        """Test creating an instance with non-default fields"""
+        """Test creating an instance with non-default fields."""
         one_of_field_test = OneofFieldTest(int_field_1=10, normal_field=False)
         self.assertEqual(one_of_field_test.int_field_1, 10)
         self.assertIsNone(one_of_field_test.int_field_2)
         self.assertFalse(one_of_field_test.normal_field)
 
     def test_set_oneof_field(self):
-        """Tests setting oneof fields causes others in the group to be unset"""
+        """Tests setting oneof fields causes others in the group to be unset."""
         one_of_field_test = OneofFieldTest()
         one_of_field_test.int_field_1 = 10
         self.assertEqual(one_of_field_test.int_field_1, 10)
@@ -128,8 +128,8 @@
         self.assertIsNone(one_of_field_test.int_field_1)
         self.assertEqual(one_of_field_test.int_field_2, 20)
 
-        # Now create a new instance and make sure changes to it are not reflected
-        # on the original object.
+        # Now create a new instance and make sure changes to it are not
+        # reflected on the original object.
         one_of_field_test_2 = OneofFieldTest()
         one_of_field_test_2.int_field_1 = 1000
         self.assertEqual(one_of_field_test_2.int_field_1, 1000)
@@ -138,7 +138,7 @@
         self.assertEqual(one_of_field_test.int_field_2, 20)
 
     def test_set_to_none(self):
-        """Tests explicitly setting a oneof field to None"""
+        """Tests explicitly setting a oneof field to None."""
         one_of_field_test = OneofFieldTest(int_field_1=10, normal_field=False)
         self.assertEqual(one_of_field_test.int_field_1, 10)
         self.assertIsNone(one_of_field_test.int_field_2)
@@ -163,7 +163,7 @@
         self.assertFalse(one_of_field_test.normal_field)
 
     def test_oneof_specs(self):
-        """Tests the `oneof_field_specs` filter"""
+        """Tests the `oneof_field_specs` filter."""
         expected = {
             "int_field_1": ir_data_fields.make_field_spec(
                 "int_field_1", int, ir_data_fields.FieldContainer.OPTIONAL, "type_1"
@@ -178,7 +178,7 @@
         self.assertDictEqual(actual, expected)
 
     def test_oneof_mappings(self):
-        """Tests the `oneof_mappings` function"""
+        """Tests the `oneof_mappings` function."""
         expected = (("int_field_1", "type_1"), ("int_field_2", "type_1"))
         actual = ir_data_fields.IrDataclassSpecs.get_specs(
             OneofFieldTest
@@ -187,7 +187,13 @@
 
 
 class IrDataFieldsTest(unittest.TestCase):
-    """Tests misc methods in ir_data_fields"""
+    """Tests misc methods in ir_data_fields."""
+
+    def assertEmpty(self, obj):
+        self.assertEqual(len(obj), 0, msg=f"{obj} is not empty.")
+
+    def assertLen(self, obj, length):
+        self.assertEqual(len(obj), length, msg=f"{obj} has length {len(obj)}.")
 
     def assertEmpty(self, obj):
         self.assertEqual(len(obj), 0, msg=f"{obj} is not empty.")
@@ -196,7 +202,7 @@
         self.assertEqual(len(obj), length, msg=f"{obj} has length {len(obj)}.")
 
     def test_copy(self):
-        """Tests copying a data class works as expected"""
+        """Tests copying a data class works as expected."""
         union = ClassWithTwoUnions(
             opaque=Opaque(), boolean=True, non_union_field=10, seq_field=[1, 2, 3]
         )
@@ -210,7 +216,7 @@
         self.assertIsNone(empty_copy)
 
     def test_copy_values_list(self):
-        """Tests that CopyValuesList copies values"""
+        """Tests that CopyValuesList copies values."""
         data_list = ir_data_fields.CopyValuesList(ListCopyTestClass)
         self.assertEmpty(data_list)
 
@@ -222,7 +228,7 @@
             self.assertEqual(i, list_test)
 
     def test_list_param_is_copied(self):
-        """Test that lists passed to constructors are converted to CopyValuesList"""
+        """Test that lists passed to constructors are converted to CopyValuesList."""
         seq_field = [5, 6, 7]
         list_test = ListCopyTestClass(non_union_field=2, seq_field=seq_field)
         self.assertLen(list_test.seq_field, len(seq_field))
diff --git a/compiler/util/ir_data_utils.py b/compiler/util/ir_data_utils.py
index 154f9d0..99fbf7d 100644
--- a/compiler/util/ir_data_utils.py
+++ b/compiler/util/ir_data_utils.py
@@ -91,7 +91,7 @@
 
 
 class IrDataSerializer:
-    """Provides methods for serializing IR data objects"""
+    """Provides methods for serializing IR data objects."""
 
     def __init__(self, ir: MessageT):
         assert ir is not None
@@ -102,6 +102,7 @@
         ir: MessageT,
         field_func: Callable[[MessageT], list[Tuple[ir_data_fields.FieldSpec, Any]]],
     ) -> MutableMapping[str, Any]:
+        """Translates the IR to a standard Python `dict`."""
         assert ir is not None
         values: MutableMapping[str, Any] = {}
         for spec, value in field_func(ir):
@@ -117,12 +118,12 @@
         """Converts the IR data class to a dictionary."""
 
         def non_empty(ir):
-            return fields_and_values(
+            return _fields_and_values(
                 ir, lambda v: v is not None and (not isinstance(v, list) or len(v))
             )
 
         def all_fields(ir):
-            return fields_and_values(ir)
+            return _fields_and_values(ir)
 
         # It's tempting to use `dataclasses.asdict` here, but that does a deep
         # copy which is overkill for the current usage; mainly as an intermediary
@@ -130,17 +131,17 @@
         return self._to_dict(self.ir, non_empty if exclude_none else all_fields)
 
     def to_json(self, *args, **kwargs):
-        """Converts the IR data class to a JSON string"""
+        """Converts the IR data class to a JSON string."""
         return json.dumps(self.to_dict(exclude_none=True), *args, **kwargs)
 
     @staticmethod
     def from_json(data_cls, data):
-        """Constructs an IR data class from the given JSON string"""
+        """Constructs an IR data class from the given JSON string."""
         as_dict = json.loads(data)
         return IrDataSerializer.from_dict(data_cls, as_dict)
 
     def copy_from_dict(self, data):
-        """Deserializes the data and overwrites the IR data class with it"""
+        """Deserializes the data and overwrites the IR data class with it."""
         cls = type(self.ir)
         data_copy = IrDataSerializer.from_dict(cls, data)
         for k in field_specs(cls):
@@ -148,6 +149,7 @@
 
     @staticmethod
     def _enum_type_converter(enum_cls: type[enum.Enum], val: Any) -> enum.Enum:
+        """Converts `val` to an instance of `enum_cls`."""
         if isinstance(val, str):
             return getattr(enum_cls, val)
         return enum_cls(val)
@@ -158,6 +160,7 @@
 
     @staticmethod
     def _from_dict(data_cls: type[MessageT], data):
+        """Translates the given `data` dict to an instance of `data_cls`."""
         class_fields: MutableMapping[str, Any] = {}
         for name, spec in ir_data_fields.field_specs(data_cls).items():
             if (value := data.get(name)) is not None:
@@ -188,12 +191,12 @@
 
     @staticmethod
     def from_dict(data_cls: type[MessageT], data):
-        """Creates a new IR data instance from a serialized dict"""
+        """Creates a new IR data instance from a serialized dict."""
         return IrDataSerializer._from_dict(data_cls, data)
 
 
 class _IrDataSequenceBuilder(MutableSequence[MessageT]):
-    """Wrapper for a list of IR elements
+    """Wrapper for a list of IR elements.
 
     Simply wraps the returned values during indexed access and iteration with
     IrDataBuilders.
@@ -236,7 +239,7 @@
 
 
 class _IrDataBuilder(Generic[MessageT]):
-    """Wrapper for an IR element"""
+    """Wrapper for an IR element."""
 
     def __init__(self, ir: MessageT) -> None:
         assert ir is not None
@@ -254,10 +257,15 @@
     def __getattribute__(self, name: str) -> Any:
         """Hook for `getattr` that handles adding missing fields.
 
-        If the field is missing inserts it, and then returns either the raw value
-        for basic types
-        or a new IrBuilder wrapping the field to handle the next field access in a
-        longer chain.
+        If the field is missing inserts it, and then returns either the raw
+        value for basic types or a new IrBuilder wrapping the field to handle
+        the next field access in a longer chain.
+
+        Arguments:
+            name: the name of the attribute to set/retrieve
+
+        Returns:
+            The value of the attribute `name`.
         """
 
         # Check if getting one of the builder attributes
@@ -294,12 +302,12 @@
         return obj
 
     def CopyFrom(self, template: MessageT):  # pylint:disable=invalid-name
-        """Updates the fields of this class with values set in the template"""
+        """Updates the fields of this class with values set in the template."""
         update(cast(type[MessageT], self), template)
 
 
 def builder(target: MessageT) -> MessageT:
-    """Create a wrapper around the target to help build an IR Data structure"""
+    """Create a wrapper around the target to help build an IR Data structure."""
     # Check if the target is already a builder.
     if isinstance(target, (_IrDataBuilder, _IrDataSequenceBuilder)):
         return target
@@ -314,7 +322,7 @@
 
 
 def _field_checker_from_spec(spec: ir_data_fields.FieldSpec):
-    """Helper that builds an FieldChecker that pretends to be an IR class"""
+    """Helper that builds an FieldChecker that pretends to be an IR class."""
     if spec.is_sequence:
         return []
     if spec.is_dataclass:
@@ -322,14 +330,15 @@
     return ir_data_fields.build_default(spec)
 
 
-def _field_type(ir_or_spec: Union[MessageT, ir_data_fields.FieldSpec]) -> type:
+def _field_type(ir_or_spec: Union[MessageT, ir_data_fields.FieldSpec]) -> type[Any]:
+    """Returns the Python type of the given field."""
     if isinstance(ir_or_spec, ir_data_fields.FieldSpec):
         return ir_or_spec.data_type
     return type(ir_or_spec)
 
 
 class _ReadOnlyFieldChecker:
-    """Class used the chain calls to fields that aren't set"""
+    """Class used to chain calls to fields that aren't set."""
 
     def __init__(self, ir_or_spec: Union[MessageT, ir_data_fields.FieldSpec]) -> None:
         self.ir_or_spec = ir_or_spec
@@ -382,8 +391,7 @@
 
 
 def reader(obj: Union[MessageT, _ReadOnlyFieldChecker]) -> MessageT:
-    """Builds a read-only wrapper that can be used to check chains of possibly
-    unset fields.
+    """Builds a wrapper that can be used to read chains of possibly unset fields.
 
     This wrapper explicitly does not alter the wrapped object and is only
     intended for reading contents.
@@ -391,18 +399,36 @@
     For example, a `reader` lets you do:
     ```
     def get_function_name_end_column(function: ir_data.Function):
-      return reader(function).function_name.source_location.end.column
+        return reader(function).function_name.source_location.end.column
     ```
 
     Instead of:
     ```
     def get_function_name_end_column(function: ir_data.Function):
-      if function.function_name:
-        if function.function_name.source_location:
-          if function.function_name.source_location.end:
-            return function.function_name.source_location.end.column
-      return 0
+        if function.function_name:
+            if function.function_name.source_location:
+                if function.function_name.source_location.end:
+                    return function.function_name.source_location.end.column
+        return 0
     ```
+
+    Arguments:
+        obj: The IR node to wrap.
+
+    Returns:
+        An object whose attributes return either:
+
+        The value of `obj.attr` if `attr` is an atomic type and is set on
+        `obj`.
+
+        A default value for `obj.attr` if `obj.attr` is not set, but is of an
+        atomic type.
+
+        A read-only wrapper around `obj.attr` if `obj.attr` is set and is an IR
+        node type.
+
+        A read-only wrapper around an empty IR node object if `obj.attr` is not
+        set, and is of an IR node type.
     """
     # Create a read-only wrapper if it's not already one.
     if not isinstance(obj, _ReadOnlyFieldChecker):
@@ -426,15 +452,19 @@
     return cast(ir_data_fields.IrDataclassInstance, ir_or_wrapper)
 
 
-def fields_and_values(
+def _fields_and_values(
     ir_wrapper: Union[MessageT, _ReadOnlyFieldChecker],
     value_filt: Optional[Callable[[Any], bool]] = None,
 ) -> list[Tuple[ir_data_fields.FieldSpec, Any]]:
     """Retrieves the fields and their values for a given IR data class.
 
     Args:
-      ir: The IR data class or a read-only wrapper of an IR data class.
-      value_filt: Optional filter used to exclude values.
+        ir: The IR data class or a read-only wrapper of an IR data class.
+        value_filt: Optional filter used to exclude values.
+
+    Returns:
+        Fields and their values for the IR held by `ir_wrapper`, optionally
+        filtered by `value_filt`.
     """
     if (ir := _extract_ir(ir_wrapper)) is None:
         return []
@@ -443,15 +473,22 @@
 
 
 def get_set_fields(ir: MessageT):
-    """Retrieves the field spec and value of fields that are set in the given IR data class.
+    """Retrieves the field specs and values of fields that are set in `ir`.
 
     A value is considered "set" if it is not None.
+
+    Arguments:
+        ir: The IR node to operate on.
+
+    Returns:
+        The field specs and values of fields that are set in the given IR data
+        class.
     """
-    return fields_and_values(ir, lambda v: v is not None)
+    return _fields_and_values(ir, lambda v: v is not None)
 
 
 def copy(ir_wrapper: Optional[MessageT]) -> Optional[MessageT]:
-    """Creates a copy of the given IR data class"""
+    """Creates a copy of the given IR data class."""
     if (ir := _extract_ir(ir_wrapper)) is None:
         return None
     ir_copy = ir_data_fields.copy(ir)
diff --git a/compiler/util/ir_data_utils_test.py b/compiler/util/ir_data_utils_test.py
index c6e0435..19ae579 100644
--- a/compiler/util/ir_data_utils_test.py
+++ b/compiler/util/ir_data_utils_test.py
@@ -35,12 +35,12 @@
 
 @dataclasses.dataclass
 class Opaque(ir_data.Message):
-    """Used for testing data field helpers"""
+    """Used for testing data field helpers."""
 
 
 @dataclasses.dataclass
 class ClassWithUnion(ir_data.Message):
-    """Used for testing data field helpers"""
+    """Used for testing data field helpers."""
 
     opaque: Optional[Opaque] = ir_data_fields.oneof_field("type")
     integer: Optional[int] = ir_data_fields.oneof_field("type")
@@ -51,7 +51,7 @@
 
 @dataclasses.dataclass
 class ClassWithTwoUnions(ir_data.Message):
-    """Used for testing data field helpers"""
+    """Used for testing data field helpers."""
 
     opaque: Optional[Opaque] = ir_data_fields.oneof_field("type_1")
     integer: Optional[int] = ir_data_fields.oneof_field("type_1")
@@ -65,7 +65,7 @@
     """Tests for the miscellaneous utility functions in ir_data_utils.py."""
 
     def test_field_specs(self):
-        """Tests the `field_specs` method"""
+        """Tests the `field_specs` method."""
         fields = ir_data_utils.field_specs(ir_data.TypeDefinition)
         self.assertIsNotNone(fields)
         expected_fields = (
@@ -132,7 +132,7 @@
         self.assertEqual(fields["base_type"], expected_field)
 
     def test_is_sequence(self):
-        """Tests for the `FieldSpec.is_sequence` helper"""
+        """Tests for the `FieldSpec.is_sequence` helper."""
         type_def = ir_data.TypeDefinition(
             attribute=[
                 ir_data.Attribute(
@@ -151,7 +151,7 @@
         self.assertFalse(fields["is_default"].is_sequence)
 
     def test_is_dataclass(self):
-        """Tests FieldSpec.is_dataclass against ir_data"""
+        """Tests FieldSpec.is_dataclass against ir_data."""
         type_def = ir_data.TypeDefinition(
             attribute=[
                 ir_data.Attribute(
@@ -173,7 +173,7 @@
         self.assertFalse(fields["fields_in_dependency_order"].is_dataclass)
 
     def test_get_set_fields(self):
-        """Tests that get set fields works"""
+        """Tests that get set fields works."""
         type_def = ir_data.TypeDefinition(
             attribute=[
                 ir_data.Attribute(
@@ -196,7 +196,7 @@
         self.assertSetEqual(found_fields, expected_fields)
 
     def test_copy(self):
-        """Tests the `copy` helper"""
+        """Tests the `copy` helper."""
         attribute = ir_data.Attribute(
             value=ir_data.AttributeValue(expression=ir_data.Expression()),
             name=ir_data.Word(text="phil"),
@@ -219,7 +219,7 @@
         self.assertIsNot(type_def.attribute, type_def_copy.attribute)
 
     def test_update(self):
-        """Tests the `update` helper"""
+        """Tests the `update` helper."""
         attribute_template = ir_data.Attribute(
             value=ir_data.AttributeValue(expression=ir_data.Expression()),
             name=ir_data.Word(text="phil"),
@@ -236,7 +236,13 @@
 
 
 class IrDataBuilderTest(unittest.TestCase):
-    """Tests for IrDataBuilder"""
+    """Tests for IrDataBuilder."""
+
+    def assertEmpty(self, obj):
+        self.assertEqual(len(obj), 0, msg=f"{obj} is not empty.")
+
+    def assertLen(self, obj, length):
+        self.assertEqual(len(obj), length, msg=f"{obj} has length {len(obj)}.")
 
     def assertEmpty(self, obj):
         self.assertEqual(len(obj), 0, msg=f"{obj} is not empty.")
@@ -245,7 +251,7 @@
         self.assertEqual(len(obj), length, msg=f"{obj} has length {len(obj)}.")
 
     def test_ir_data_builder(self):
-        """Tests that basic builder chains work"""
+        """Tests that basic builder chains work."""
         # We start with an empty type
         type_def = ir_data.TypeDefinition()
         self.assertFalse(type_def.HasField("name"))
@@ -264,7 +270,7 @@
         self.assertEqual(type_def.name.name.text, "phil")
 
     def test_ir_data_builder_bad_field(self):
-        """Tests accessing an undefined field name fails"""
+        """Tests accessing an undefined field name fails."""
         type_def = ir_data.TypeDefinition()
         builder = ir_data_utils.builder(type_def)
         self.assertRaises(AttributeError, lambda: builder.foo)
@@ -272,7 +278,7 @@
         self.assertRaises(AttributeError, getattr, type_def, "foo")
 
     def test_ir_data_builder_sequence(self):
-        """Tests that sequences are properly wrapped"""
+        """Tests that sequences are properly wrapped."""
         # We start with an empty type
         type_def = ir_data.TypeDefinition()
         self.assertTrue(type_def.HasField("attribute"))
@@ -374,7 +380,7 @@
         )
 
     def test_ir_data_builder_sequence_scalar(self):
-        """Tests that sequences of scalars function properly"""
+        """Tests that sequences of scalars function properly."""
         # We start with an empty type
         structure = ir_data.Structure()
 
@@ -410,10 +416,10 @@
 
 
 class IrDataSerializerTest(unittest.TestCase):
-    """Tests for IrDataSerializer"""
+    """Tests for IrDataSerializer."""
 
     def test_ir_data_serializer_to_dict(self):
-        """Tests serialization with `IrDataSerializer.to_dict` with default settings"""
+        """Tests serialization with `IrDataSerializer.to_dict` with default settings."""
         attribute = ir_data.Attribute(
             value=ir_data.AttributeValue(expression=ir_data.Expression()),
             name=ir_data.Word(text="phil"),
@@ -444,7 +450,7 @@
         self.assertDictEqual(raw_dict, expected)
 
     def test_ir_data_serializer_to_dict_exclude_none(self):
-        """Tests serialization with `IrDataSerializer.to_dict` when excluding None values"""
+        """.Tests serialization with `IrDataSerializer.to_dict` when excluding None values"""
         attribute = ir_data.Attribute(
             value=ir_data.AttributeValue(expression=ir_data.Expression()),
             name=ir_data.Word(text="phil"),
@@ -455,7 +461,7 @@
         self.assertDictEqual(raw_dict, expected)
 
     def test_ir_data_serializer_to_dict_enum(self):
-        """Tests that serialization of `enum.Enum` values works properly"""
+        """Tests that serialization of `enum.Enum` values works properly."""
         type_def = ir_data.TypeDefinition(addressable_unit=ir_data.AddressableUnit.BYTE)
         serializer = ir_data_utils.IrDataSerializer(type_def)
         raw_dict = serializer.to_dict(exclude_none=True)
@@ -463,7 +469,7 @@
         self.assertDictEqual(raw_dict, expected)
 
     def test_ir_data_serializer_from_dict(self):
-        """Tests deserializing IR data from a serialized dict"""
+        """Tests deserializing IR data from a serialized dict."""
         attribute = ir_data.Attribute(
             value=ir_data.AttributeValue(expression=ir_data.Expression()),
             name=ir_data.Word(text="phil"),
@@ -474,7 +480,7 @@
         self.assertEqual(attribute, new_attribute)
 
     def test_ir_data_serializer_from_dict_enum(self):
-        """Tests that deserializing `enum.Enum` values works properly"""
+        """Tests that deserializing `enum.Enum` values works properly."""
         type_def = ir_data.TypeDefinition(addressable_unit=ir_data.AddressableUnit.BYTE)
 
         serializer = ir_data_utils.IrDataSerializer(type_def)
@@ -483,7 +489,7 @@
         self.assertEqual(type_def, new_type_def)
 
     def test_ir_data_serializer_from_dict_enum_is_str(self):
-        """Tests that deserializing `enum.Enum` values works properly when string constant is used"""
+        """Tests that deserializing `enum.Enum` values works properly when string constant is used."""
         type_def = ir_data.TypeDefinition(addressable_unit=ir_data.AddressableUnit.BYTE)
         raw_dict = {"addressable_unit": "BYTE"}
         serializer = ir_data_utils.IrDataSerializer(type_def)
@@ -491,7 +497,7 @@
         self.assertEqual(type_def, new_type_def)
 
     def test_ir_data_serializer_from_dict_exclude_none(self):
-        """Tests that deserializing from a dict that excluded None values works properly"""
+        """Tests that deserializing from a dict that excluded None values works properly."""
         attribute = ir_data.Attribute(
             value=ir_data.AttributeValue(expression=ir_data.Expression()),
             name=ir_data.Word(text="phil"),
@@ -558,7 +564,7 @@
         self.assertIsNotNone(func)
 
     def test_ir_data_serializer_copy_from_dict(self):
-        """Tests that updating an IR data struct from a dict works properly"""
+        """Tests that updating an IR data struct from a dict works properly."""
         attribute = ir_data.Attribute(
             value=ir_data.AttributeValue(expression=ir_data.Expression()),
             name=ir_data.Word(text="phil"),
@@ -573,10 +579,10 @@
 
 
 class ReadOnlyFieldCheckerTest(unittest.TestCase):
-    """Tests the ReadOnlyFieldChecker"""
+    """Tests the ReadOnlyFieldChecker."""
 
     def test_basic_wrapper(self):
-        """Tests basic field checker actions"""
+        """Tests basic field checker actions."""
         union = ClassWithTwoUnions(opaque=Opaque(), boolean=True, non_union_field=10)
         field_checker = ir_data_utils.reader(union)
 
@@ -597,7 +603,7 @@
         self.assertTrue(field_checker.HasField("non_union_field"))
 
     def test_construct_from_field_checker(self):
-        """Tests that constructing from another field checker works"""
+        """Tests that constructing from another field checker works."""
         union = ClassWithTwoUnions(opaque=Opaque(), boolean=True, non_union_field=10)
         field_checker_orig = ir_data_utils.reader(union)
         field_checker = ir_data_utils.reader(field_checker_orig)
@@ -621,7 +627,7 @@
         self.assertTrue(field_checker.HasField("non_union_field"))
 
     def test_read_only(self) -> None:
-        """Tests that the read only wrapper really is read only"""
+        """Tests that the read only wrapper really is read only."""
         union = ClassWithTwoUnions(opaque=Opaque(), boolean=True, non_union_field=10)
         field_checker = ir_data_utils.reader(union)
 
diff --git a/compiler/util/name_conversion.py b/compiler/util/name_conversion.py
index 0344e8c..60a5c86 100644
--- a/compiler/util/name_conversion.py
+++ b/compiler/util/name_conversion.py
@@ -63,10 +63,19 @@
 def convert_case(case_from, case_to, value):
     """Converts cases based on runtime case values.
 
-    Note: Cases can be strings or enum values."""
+    Note: Cases can be strings or enum values.
+
+    Arguments:
+        case_from: the name of the original case
+        case_to: the name of the desired case
+        value: the value to convert
+
+    Returns:
+        `value` converted from `case_from` to `case_to`.
+    """
     return _case_conversions[case_from, case_to](value)
 
 
 def is_case_conversion_supported(case_from, case_to):
-    """Determines if a case conversion would be supported"""
+    """Determines if a case conversion would be supported."""
     return (case_from, case_to) in _case_conversions
diff --git a/compiler/util/traverse_ir.py b/compiler/util/traverse_ir.py
index f79e4bd..5a5ac78 100644
--- a/compiler/util/traverse_ir.py
+++ b/compiler/util/traverse_ir.py
@@ -26,8 +26,9 @@
     """Provides a template for setting up a generic call to a function.
 
     The function parameters are inspected at run-time to build up a set of valid
-    and required arguments. When invoking the function unneccessary parameters
-    will be trimmed out. If arguments are missing an assertion will be triggered.
+    and required arguments. When invoking the function unnecessary parameters
+    will be trimmed out. If arguments are missing an assertion will be
+    triggered.
 
     This is currently limited to functions that have at least one positional
     parameter.
diff --git a/doc/cpp-reference.md b/doc/cpp-reference.md
index 7938587..d480d61 100644
--- a/doc/cpp-reference.md
+++ b/doc/cpp-reference.md
@@ -173,7 +173,7 @@
 ```
 
 The `IntrinsicSizeInBytes` method is the [field method](#struct-field-methods)
-for [`$size_in_bytes`](language-reference.md#size-in-bytes).  The `Read` method
+for [`$size_in_bytes`](language-reference.md#size_in_bytes).  The `Read` method
 of the result returns the size of the `struct`, and the `Ok` method returns
 `true` if the `struct`'s intrinsic size is known; i.e.:
 
@@ -212,7 +212,7 @@
 ```
 
 The `MaxSizeInBytes` method is the [field method](#struct-field-methods)
-for [`$max_size_in_bytes`](language-reference.md#max-size-in-bytes).  The `Read`
+for [`$max_size_in_bytes`](language-reference.md#max_size_in_bytes).  The `Read`
 method of the result returns the maximum size of the `struct`, and the `Ok`
 always method returns `true`.
 
@@ -250,7 +250,7 @@
 ```
 
 The `MinSizeInBytes` method is the [field method](#struct-field-methods)
-for [`$min_size_in_bytes`](language-reference.md#max-size-in-bytes).  The `Read`
+for [`$min_size_in_bytes`](language-reference.md#min_size_in_bytes).  The `Read`
 method of the result returns the minimum size of the `struct`, and the `Ok`
 always method returns `true`.
 
@@ -367,7 +367,7 @@
 is a template parameter on the view.
 
 
-### Field methods {#struct-field-methods}
+### `struct` field methods
 
 Each physical field and virtual field in the `struct` will have a corresponding
 method in the generated view for that `struct`, which returns a subview of that
@@ -635,7 +635,7 @@
 ```
 
 The `IntrinsicSizeInBits` method is the [field method](#bits-field-methods) for
-[`$size_in_bits`](language-reference.md#size-in-bits).  The `Read` method of
+[`$size_in_bits`](language-reference.md#size_in_bits).  The `Read` method of
 the result returns the size of the `struct`, and the `Ok` method returns `true`
 if the `struct`'s intrinsic size is known; i.e.:
 
@@ -666,7 +666,7 @@
 ```
 
 The `MaxSizeInBits` method is the [field method](#struct-field-methods)
-for [`$max_size_in_bits`](language-reference.md#max-size-in-bits).  The `Read`
+for [`$max_size_in_bits`](language-reference.md#max_size_in_bits).  The `Read`
 method of the result returns the maximum size of the `bits`, and the `Ok`
 always method returns `true`.
 
@@ -704,7 +704,7 @@
 ```
 
 The `MinSizeInBits` method is the [field method](#struct-field-methods)
-for [`$min_size_in_bits`](language-reference.md#min-size-in-bits).  The `Read`
+for [`$min_size_in_bits`](language-reference.md#min_size_in_bits).  The `Read`
 method of the result returns the minimum size of the `bits`, and the `Ok`
 always method returns `true`.
 
@@ -773,7 +773,7 @@
 would not call this directly; instead, use the global `WriteToString` method,
 which handles setting up the stream and returning the resulting string.
 
-### Field methods {#bits-field-methods}
+### `bits` field methods
 
 As with `struct`, each field in a `bits` will have a corresponding method of the
 same name generated, and each such method will return a view of the given field.
diff --git a/doc/design_docs/bit_shift.md b/doc/design_docs/bit_shift.md
new file mode 100644
index 0000000..9eb6447
--- /dev/null
+++ b/doc/design_docs/bit_shift.md
@@ -0,0 +1,295 @@
+# Design Sketch: Left and Right Bit Shift Operators
+
+## Overview
+
+This is a proposal to add left and right shift to the Emboss expression
+language.
+
+Bit shifts are common in embedded systems work.  Sometimes, the lack of bit
+shifts in Emboss can be worked around, such as:
+
+```
+bits Control:
+  0 [+8] UInt control_byte
+  0 [+4] UInt frame_type_1
+  4 [+1] UInt p_f
+  5 [+3] UInt frame_type_2
+
+  # frame_type_1 | (frame_type_2 << 5)
+  let frame_type = frame_type_1 + frame_type_2 * 32
+```
+
+However, this is awkward, and only works for left shift by a constant value.
+
+It would also be useful to add bitwise and, or, xor, and not operators, but
+those are not in scope for this proposal.
+
+
+## Syntax
+
+### Symbol
+
+The symbols `<<` and `>>` are used for bit shift operators in many common
+programming languages.  For `>>`, the choice of whether to sign-extend or
+zero-extend ("arithmetic" or "unsigned") usually depends on the type of the
+left operand.  Java, C#, and JavaScript have an additional "unsigned right
+shift" operator `>>>`, which I believe is due to the (historical) lack of an
+unsigned integer type in those languages.
+
+Other than `<<` and `>>`, other languages use functions with language-specific
+names.
+
+| Language   | Left Shift        | Right Shift (RS)                       |
+| ---------- | ----------------- | -------------------------------------- |
+| ASM (x86)  | `SAL`/`SHL`       | `SAR` (arithmetic), `SHR` (unsigned)   |
+| ASM (ARM)  | `LSLV`            | `ASRV` (arithmetic), `LSRV` (unsigned) |
+| C          | `<<`              | `>>`                                   |
+| C++        | `<<`              | `>>`                                   |
+| C#         | `<<`              | `>>` (arithmetic), `>>>` (unsigned)    |
+| Go         | `<<`              | `>>`                                   |
+| Fortran    | `SHIFTL()`        | `SHIFTA()` (arithmetic), `SHIFTR()` (unsigned) |
+| Haskell    | `shift`, `shiftL` | `shift` by negative, `shiftR`          |
+| Java       | `<<`              | `>>` (arithmetic), `>>>` (unsigned)    |
+| JavaScript | `<<`              | `>>` (arithmetic), `>>>` (unsigned)    |
+| Lua        | `<<`              | `>>` (unsigned)                        |
+| MatLab     | `bitshift(A, k)`  | `bitshift(A, -k)`                      |
+| OCaml      | `shift_left`      | `shift_right`                          |
+| Perl       | `<<`              | `>>`                                   |
+| PHP        | `<<`              | `>>` (arithmetic)                      |
+| Python     | `<<`              | `>>`                                   |
+| Rust       | `<<`              | `>>`                                   |
+| SQL        | Nonstandard       | Nonstandard                            | 
+
+
+#### Proposal
+
+Emboss should use `<<` and `>>` operators for left and right shift.  For now,
+`>>` should always be arithmetic right shift: since integers in the Emboss
+expression language are notionally infinite-precision, there is not a 'natural'
+number of bits at which to cut off negative numbers.
+
+Once/if bitwise operators are implemented, unsigned right shift can be handled
+by forcing the left operand into an unsigned representation using `&`.
+
+
+### Precedence
+
+The precedence of `>>` and `<<` is consistent across many languages, sitting
+between additive operators (binary `+` and `-`) and comparison operators (`<`,
+`>`, and so on).
+
+There is inconsistency in the precedence of `&`, `^`, and `|`, with many
+languages taking C's choice (binary bitwise operators bind less tightly than
+comparisons) and a notable minority fixing C's mistake (by putting bitwise
+operators between shifts and comparisons).
+
+
+#### C, C++, Perl, JavaScript, Java, & C#
+
+1. `+` `-`
+2. `<<` `>>` `>>>`
+3. `<` `<=` `>` `>=`
+4. `==` `!=
+5. `&`
+6. `^`
+7. `|`
+
+
+#### Rust, Python, & Lua
+
+1. `+` `-`
+2. `<<` `>>
+3. `&`
+4. `^`/`~`
+5. `|`
+6. `==` `!=`/`~=` `<` `>` `<=` `>=`
+
+
+#### Go
+
+1. `*` `/` `%` `<<` `>>` `&` `&^`
+2. `+` `-` `|` `^`
+3. `==` `!=` `<` `<=` `>` `>=`
+4. `&&`
+5. `||`
+
+
+#### Straw Poll
+
+An informal poll of other developers found that some developers have a mental
+model roughly equivalent to:
+
+1. `+` `-` `<<` `>>`
+2. `<` `<=` `>` `>=`
+
+In particular, the expression `5 + 11 >> 1 + 1` was thought to evaluate to 11
+by some, and 4 (as in all common programming languages other than Go) by
+others.
+
+
+#### Proposal
+
+Emboss's operator precedence is not a strict ordering: certain operators (such
+as `&&` and `||`) are neither higher, lower, or equal in precedence to each
+other, and as such cannot be mixed without parentheses.
+
+Given the developer confusion around `>>` vs `+`, Emboss's precedence should be
+(changes in **bold**):
+
+1.  `()` `$max()` `$present()` `$upper_bound()` `$lower_bound()`
+2.  unary `+` and `-` (cannot be combined without parentheses)
+3.  `*`
+4.  {`+` `-`} **/ {`<<` `>>`} (`+` and `-` cannot be combined with `>>` or `<<`
+    without parentheses)**
+5.  {`!=`} / {`<` `<=` `==`} / {`>` `>=` `==`} (comparisons in the same
+    direction can be *chained*; comparisons in opposite directions cannot be
+    combined without parentheses)
+6.  {`&&`} / {`||`} (`&&` and `||` cannot be combined without parentheses)
+7.  `?:`
+
+In diagram form:
+
+![The same information as above, in the form of a graph](./bit_shift/precedence.svg)
+
+
+## Semantics
+
+### Shifts by 0, Negative, or Large Values
+
+It appears that all (common?) languages properly handle shifts by 0, treating
+them as a no-op.
+
+Shifts by values equal to or larger than the width of the left operand (e.g.,
+64 bits on a 64 bit system) are split between treating them as an error, shift
+by the low-order bits of the right operand (e.g., mask to low 6 bits for 64-bit
+operands), or return 0 or -1.  Common hardware appears to mask to the low bits.
+
+Shifts by negative values are similarly split between treating them as an
+error, shift by the low-order bits of the right operand (so a shift by -1 is
+equivalent to a shift by 63), or shift in the opposite direction.
+
+One particularly notable issue is that C++ (until C++20) and C both treat left
+shift as undefined when the left operand is negative.  This is contrary to
+every other language checked, including x86 and ARM assembly (both 32- and
+64-bit).
+
+In the table below, "UB" indicates undefined behavior (e.g., a program that
+performs that operation is not valid), and "IDB" indicates
+implementation-defined behavior (a program that performs the operation is
+valid, but the result varies depending on the implementation).
+
+| Language   | `x>>0` | `x<<0` | `-2<<x` | `x<<64`  | `x>>64`  | `x>>-1`   |
+| ---------- | ------ | ------ | ------- | -------- | -------- | --------- |
+| ASM (x86)  | `x`    | `x`    | `-2<<x` | `x<<0`   | `x>>0`   | `x>>63`   |
+| ASM (ARM)  | `x`    | `x`    | `-2<<x` | `x<<0`   | `x>>0`   | `x>>63`   |
+| C++11      | `x`    | `x`    | UB      | UB       | UB       | UB        |
+| C++20      | `x`    | `x`    | `-2<<x` | UB       | UB       | UB        |
+| C          | `x`    | `x`    | UB      | UB       | UB       | UB        |
+| C#         | `x`    | `x`    | `-2<<x` | `x<<0`   | `x>>0`   | `x>>63`   |
+| Go         | `x`    | `x`    | `-2<<x` | `0`      | `0`      | ?         |
+| Fortran    | `x`    | `x`    | `-2<<x` | UB       | UB       | UB        |
+| Haskell    | `x`    | `x`    | `-2<<x` | `0`      | `0`      | Panic     |
+| Java       | `x`    | `x`    | `-2<<x` | `x<<0`   | `x>>0`   | `x>>63`   |
+| JavaScript | `x`    | `x`    | `-2<<x` | `x<<0`   | `x>>0`   | `x>>63`   |
+| Lua        | `x`    | `x`    | `-2<<x` | `0`      | `0`      | `x<<1`    |
+| OCaml      | `x`    | `x`    | `-2<<x` | IDB      | IDB      | IDB       |
+| Perl       | `x`    | `x`    | `-2<<x` | Varies   | `x>>64`  | `x<<1`    |
+| PHP        | `x`    | `x`    | `-2<<x` | `0`      | `0`      | Exception |
+| Python     | `x`    | `x`    | `-2<<x` | `x>>64`  | `x>>64`  | Exception |
+| Rust       | `x`    | `x`    | `-2<<x` | UB       | UB       | Overflow  |
+
+
+#### Proposal
+
+Emboss can use its bounds system to ensure that no shifts by too-large or
+negative amounts can happen at runtime.  This ensures that no surprising or
+undefined behavior can occur in generated code.
+
+This may prove to be overly restrictive, but it can be loosened in the future,
+if necessary.
+
+
+### Expression Bounds and Modular Value Calculations
+
+#### Left Shift
+
+For a shift `x << y`, there are several cases to consider:
+
+1.  `x` and `y` are both constants: in this case, the result is also a
+    (pre-computed) constant.
+2.  Only `y` is a constant: when `y` is a constant, `x << y` is equivalent to
+    `x` × 2<sup>y</sup>.  The minimum value, maximum value, modulus, and
+    modular value of the result are all equal to 2<sup>y</sup> multiplied by
+    the respective bound on `x`.
+3.  Otherwise:
+    *   `minimum_value` is either `minimum value of x << maximum value of y` or
+        `minimum value of x << minimum value of y`, depending on signs
+    *   `maximum_value` is either `maximum value of x << minimum value of y` or
+        `maximum value of x << maximum value of y`, depending on signs
+    *   `modular_value` is 0
+    *   `modulus` is:
+        *   `infinity` if the `modulus` of `x` is `infinity` and the
+            `modular_value` of `x` is 0
+        *   2<sup>minimum value of `y`</sup> × (largest power-of-2 factor of
+            the `modular_value` of `x`) if the `modulus` of `x` is
+            `infinity`
+        *   2<sup>minimum value of `y`</sup> × (largest power-of-2 factor of
+            the modulus of `x`) if the `modular_value` of `x` is 0
+        *   2<sup>minimum value of `y`</sup> × min(largest power-of-2 factor
+            of the `modular_value` of `x`, largest power-of-2 factor of the
+            `modulus` of `x`) if the `modular_value` of `x` is not 0
+
+These rules have been prototyped and tested against all valid bounds and
+inhabiting values of those bounds with bound values from -32 through +32,
+inclusive.
+
+
+#### Right Shift
+
+For a shift `x >> y`, there are several cases to consider:
+
+1.  `x` and `y` are both constants: in this case, the result is also a
+    (pre-computed) constant.
+2.  `y` is a constant and the `modulus` and `modular_value` of `x` are both
+    evenly divisible by 2<sup>y</sup>: in this case, the bounds of the result
+    are equal to the corresponding bounds on `x` divided by 2<sup>y</sup>.
+    Note that this is a special case of computing the bounds for
+    [division](./division_and_modulus.md#division).
+3.  Otherwise:
+    *   `minimum_value` is either `minimum value of x >> maximum value of y` or
+        `minimum value of x >> minimum value of y`, depending on signs
+    *   `maximum_value` is either `maximum value of x >> minimum value of y` or
+        `maximum value of x >> maximum value of y`, depending on signs
+    *   `modular_value` is 0
+    *   `modulus` is 1
+
+These rules have been prototyped and tested against all valid bounds and
+inhabiting values of those bounds with bound values from -32 through +32,
+inclusive.
+
+There are definitely tighter bounds that can be inferred in some cases (e.g., a
+shift of a constant `x` by a `y` that can only take 2 values will have a result
+that can only take 2 values, but will not, in general, have an inferred bound
+that only covers those two values), but these rules seem to cover most cases
+without being particularly computationally expensive.
+
+
+## Implementation Notes
+
+### Parser
+
+The parser can be modified by putting in parallel rules for `shift-`
+nonterminals wherever a rule references any `additive-` nonterminal.  This will
+automatically create a set of rules that put the `<<` and `>>` operators into a
+new precedence that is at the same level as, but cannot be mixed with, the
+additive operators.
+
+
+### Runtime
+
+The C++ implementation of the left shift operator should avoid undefined
+behavior by using `static_cast<std::make_unsigned<T>::type>()` on its operands
+before shifting, and then convert back to `T` for the result.  Note that the
+conversion back to `T` cannot (always) be done with a simple cast until C++17.
+There is some code in `IntView` that properly implements the conversion for ISO
+C++11, which should be factored out so that it can be shared.
diff --git a/doc/design_docs/bit_shift/precedence.dot b/doc/design_docs/bit_shift/precedence.dot
new file mode 100644
index 0000000..c38c220
--- /dev/null
+++ b/doc/design_docs/bit_shift/precedence.dot
@@ -0,0 +1,45 @@
+strict digraph {
+  bgcolor = "white"
+  ordering = "out"
+  edge [color = "black"]
+  node [
+    color = "black"
+    fontcolor = "black"
+    shape = box
+    ordering = "out"
+  ]
+  subgraph {
+//    rank = same
+    rankdir = LR
+
+    paren [ordering = "out"; label="() $max() $present()"]
+    unary_plus [ordering = "out"; label="unary + -"]
+    mult [ordering = "out"; label="*"]
+    add [ordering = "out"; label="+ -"]
+    shift [ordering = "out"; label=">> <<"]
+    ne [ordering = "out"; label="!="]
+    le [ordering = "out"; label="< <= =="]
+    ge [ordering = "out"; label="> >= =="]
+    land [ordering = "out"; label="&&"]
+    lor [ordering = "out"; label="||"]
+    choice [ordering = "out"; label="?:"]
+    paren -> unary_plus
+    unary_plus -> mult
+    mult -> add
+    mult -> shift
+    add -> ne
+    add -> le
+    add -> ge
+    shift -> ne
+    shift -> le
+    shift -> ge
+    ne -> land
+    ne -> lor
+    le -> land
+    le -> lor
+    ge -> land
+    ge -> lor
+    land -> choice
+    lor -> choice
+  }
+}
diff --git a/doc/design_docs/bit_shift/precedence.svg b/doc/design_docs/bit_shift/precedence.svg
new file mode 100644
index 0000000..99d9ac4
--- /dev/null
+++ b/doc/design_docs/bit_shift/precedence.svg
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.43.0 (0)
+ -->
+<!-- Title: %3 Pages: 1 -->
+<svg width="229pt" height="476pt"
+ viewBox="0.00 0.00 228.50 476.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 472)">
+<title>%3</title>
+<polygon fill="white" stroke="transparent" points="-4,4 -4,-472 224.5,-472 224.5,4 -4,4"/>
+<!-- paren -->
+<g id="node1" class="node">
+<title>paren</title>
+<polygon fill="none" stroke="black" points="169,-468 41,-468 41,-432 169,-432 169,-468"/>
+<text text-anchor="middle" x="105" y="-446.3" font-family="Times,serif" font-size="14.00">() $max() $present()</text>
+</g>
+<!-- unary_plus -->
+<g id="node2" class="node">
+<title>unary_plus</title>
+<polygon fill="none" stroke="black" points="139,-396 71,-396 71,-360 139,-360 139,-396"/>
+<text text-anchor="middle" x="105" y="-374.3" font-family="Times,serif" font-size="14.00">unary + &#45;</text>
+</g>
+<!-- paren&#45;&gt;unary_plus -->
+<g id="edge1" class="edge">
+<title>paren&#45;&gt;unary_plus</title>
+<path fill="none" stroke="black" d="M105,-431.7C105,-423.98 105,-414.71 105,-406.11"/>
+<polygon fill="black" stroke="black" points="108.5,-406.1 105,-396.1 101.5,-406.1 108.5,-406.1"/>
+</g>
+<!-- mult -->
+<g id="node3" class="node">
+<title>mult</title>
+<polygon fill="none" stroke="black" points="132,-324 78,-324 78,-288 132,-288 132,-324"/>
+<text text-anchor="middle" x="105" y="-302.3" font-family="Times,serif" font-size="14.00">*</text>
+</g>
+<!-- unary_plus&#45;&gt;mult -->
+<g id="edge2" class="edge">
+<title>unary_plus&#45;&gt;mult</title>
+<path fill="none" stroke="black" d="M105,-359.7C105,-351.98 105,-342.71 105,-334.11"/>
+<polygon fill="black" stroke="black" points="108.5,-334.1 105,-324.1 101.5,-334.1 108.5,-334.1"/>
+</g>
+<!-- add -->
+<g id="node4" class="node">
+<title>add</title>
+<polygon fill="none" stroke="black" points="96,-252 42,-252 42,-216 96,-216 96,-252"/>
+<text text-anchor="middle" x="69" y="-230.3" font-family="Times,serif" font-size="14.00">+ &#45;</text>
+</g>
+<!-- mult&#45;&gt;add -->
+<g id="edge3" class="edge">
+<title>mult&#45;&gt;add</title>
+<path fill="none" stroke="black" d="M96.1,-287.7C92,-279.73 87.05,-270.1 82.51,-261.26"/>
+<polygon fill="black" stroke="black" points="85.48,-259.4 77.8,-252.1 79.26,-262.6 85.48,-259.4"/>
+</g>
+<!-- shift -->
+<g id="node5" class="node">
+<title>shift</title>
+<polygon fill="none" stroke="black" points="168,-252 114,-252 114,-216 168,-216 168,-252"/>
+<text text-anchor="middle" x="141" y="-230.3" font-family="Times,serif" font-size="14.00">&gt;&gt; &lt;&lt;</text>
+</g>
+<!-- mult&#45;&gt;shift -->
+<g id="edge4" class="edge">
+<title>mult&#45;&gt;shift</title>
+<path fill="none" stroke="black" d="M113.9,-287.7C118,-279.73 122.95,-270.1 127.49,-261.26"/>
+<polygon fill="black" stroke="black" points="130.74,-262.6 132.2,-252.1 124.52,-259.4 130.74,-262.6"/>
+</g>
+<!-- ne -->
+<g id="node6" class="node">
+<title>ne</title>
+<polygon fill="none" stroke="black" points="54,-180 0,-180 0,-144 54,-144 54,-180"/>
+<text text-anchor="middle" x="27" y="-158.3" font-family="Times,serif" font-size="14.00">!=</text>
+</g>
+<!-- add&#45;&gt;ne -->
+<g id="edge5" class="edge">
+<title>add&#45;&gt;ne</title>
+<path fill="none" stroke="black" d="M58.62,-215.7C53.74,-207.56 47.81,-197.69 42.42,-188.7"/>
+<polygon fill="black" stroke="black" points="45.41,-186.88 37.26,-180.1 39.41,-190.48 45.41,-186.88"/>
+</g>
+<!-- le -->
+<g id="node7" class="node">
+<title>le</title>
+<polygon fill="none" stroke="black" points="137.5,-180 72.5,-180 72.5,-144 137.5,-144 137.5,-180"/>
+<text text-anchor="middle" x="105" y="-158.3" font-family="Times,serif" font-size="14.00">&lt; &lt;= ==</text>
+</g>
+<!-- add&#45;&gt;le -->
+<g id="edge6" class="edge">
+<title>add&#45;&gt;le</title>
+<path fill="none" stroke="black" d="M77.9,-215.7C82,-207.73 86.95,-198.1 91.49,-189.26"/>
+<polygon fill="black" stroke="black" points="94.74,-190.6 96.2,-180.1 88.52,-187.4 94.74,-190.6"/>
+</g>
+<!-- ge -->
+<g id="node8" class="node">
+<title>ge</title>
+<polygon fill="none" stroke="black" points="220.5,-180 155.5,-180 155.5,-144 220.5,-144 220.5,-180"/>
+<text text-anchor="middle" x="188" y="-158.3" font-family="Times,serif" font-size="14.00">&gt; &gt;= ==</text>
+</g>
+<!-- add&#45;&gt;ge -->
+<g id="edge7" class="edge">
+<title>add&#45;&gt;ge</title>
+<path fill="none" stroke="black" d="M96,-217.12C112,-207.7 132.53,-195.63 150.13,-185.28"/>
+<polygon fill="black" stroke="black" points="152.18,-188.13 159.02,-180.04 148.63,-182.1 152.18,-188.13"/>
+</g>
+<!-- shift&#45;&gt;ne -->
+<g id="edge8" class="edge">
+<title>shift&#45;&gt;ne</title>
+<path fill="none" stroke="black" d="M113.98,-216.41C98.76,-207.06 79.55,-195.27 63.06,-185.14"/>
+<polygon fill="black" stroke="black" points="64.57,-181.96 54.22,-179.71 60.91,-187.93 64.57,-181.96"/>
+</g>
+<!-- shift&#45;&gt;le -->
+<g id="edge9" class="edge">
+<title>shift&#45;&gt;le</title>
+<path fill="none" stroke="black" d="M132.1,-215.7C128,-207.73 123.05,-198.1 118.51,-189.26"/>
+<polygon fill="black" stroke="black" points="121.48,-187.4 113.8,-180.1 115.26,-190.6 121.48,-187.4"/>
+</g>
+<!-- shift&#45;&gt;ge -->
+<g id="edge10" class="edge">
+<title>shift&#45;&gt;ge</title>
+<path fill="none" stroke="black" d="M152.62,-215.7C158.14,-207.47 164.85,-197.48 170.93,-188.42"/>
+<polygon fill="black" stroke="black" points="173.85,-190.36 176.52,-180.1 168.04,-186.46 173.85,-190.36"/>
+</g>
+<!-- land -->
+<g id="node9" class="node">
+<title>land</title>
+<polygon fill="none" stroke="black" points="96,-108 42,-108 42,-72 96,-72 96,-108"/>
+<text text-anchor="middle" x="69" y="-86.3" font-family="Times,serif" font-size="14.00">&amp;&amp;</text>
+</g>
+<!-- ne&#45;&gt;land -->
+<g id="edge11" class="edge">
+<title>ne&#45;&gt;land</title>
+<path fill="none" stroke="black" d="M37.38,-143.7C42.26,-135.56 48.19,-125.69 53.58,-116.7"/>
+<polygon fill="black" stroke="black" points="56.59,-118.48 58.74,-108.1 50.59,-114.88 56.59,-118.48"/>
+</g>
+<!-- lor -->
+<g id="node10" class="node">
+<title>lor</title>
+<polygon fill="none" stroke="black" points="168,-108 114,-108 114,-72 168,-72 168,-108"/>
+<text text-anchor="middle" x="141" y="-86.3" font-family="Times,serif" font-size="14.00">||</text>
+</g>
+<!-- ne&#45;&gt;lor -->
+<g id="edge12" class="edge">
+<title>ne&#45;&gt;lor</title>
+<path fill="none" stroke="black" d="M54.02,-144.41C69.24,-135.06 88.45,-123.27 104.94,-113.14"/>
+<polygon fill="black" stroke="black" points="107.09,-115.93 113.78,-107.71 103.43,-109.96 107.09,-115.93"/>
+</g>
+<!-- le&#45;&gt;land -->
+<g id="edge13" class="edge">
+<title>le&#45;&gt;land</title>
+<path fill="none" stroke="black" d="M96.1,-143.7C92,-135.73 87.05,-126.1 82.51,-117.26"/>
+<polygon fill="black" stroke="black" points="85.48,-115.4 77.8,-108.1 79.26,-118.6 85.48,-115.4"/>
+</g>
+<!-- le&#45;&gt;lor -->
+<g id="edge14" class="edge">
+<title>le&#45;&gt;lor</title>
+<path fill="none" stroke="black" d="M113.9,-143.7C118,-135.73 122.95,-126.1 127.49,-117.26"/>
+<polygon fill="black" stroke="black" points="130.74,-118.6 132.2,-108.1 124.52,-115.4 130.74,-118.6"/>
+</g>
+<!-- ge&#45;&gt;land -->
+<g id="edge15" class="edge">
+<title>ge&#45;&gt;land</title>
+<path fill="none" stroke="black" d="M158.89,-143.88C142.6,-134.29 122.16,-122.27 104.88,-112.11"/>
+<polygon fill="black" stroke="black" points="106.57,-109.04 96.18,-106.99 103.02,-115.07 106.57,-109.04"/>
+</g>
+<!-- ge&#45;&gt;lor -->
+<g id="edge16" class="edge">
+<title>ge&#45;&gt;lor</title>
+<path fill="none" stroke="black" d="M176.38,-143.7C170.86,-135.47 164.15,-125.48 158.07,-116.42"/>
+<polygon fill="black" stroke="black" points="160.96,-114.46 152.48,-108.1 155.15,-118.36 160.96,-114.46"/>
+</g>
+<!-- choice -->
+<g id="node11" class="node">
+<title>choice</title>
+<polygon fill="none" stroke="black" points="132,-36 78,-36 78,0 132,0 132,-36"/>
+<text text-anchor="middle" x="105" y="-14.3" font-family="Times,serif" font-size="14.00">?:</text>
+</g>
+<!-- land&#45;&gt;choice -->
+<g id="edge17" class="edge">
+<title>land&#45;&gt;choice</title>
+<path fill="none" stroke="black" d="M77.9,-71.7C82,-63.73 86.95,-54.1 91.49,-45.26"/>
+<polygon fill="black" stroke="black" points="94.74,-46.6 96.2,-36.1 88.52,-43.4 94.74,-46.6"/>
+</g>
+<!-- lor&#45;&gt;choice -->
+<g id="edge18" class="edge">
+<title>lor&#45;&gt;choice</title>
+<path fill="none" stroke="black" d="M132.1,-71.7C128,-63.73 123.05,-54.1 118.51,-45.26"/>
+<polygon fill="black" stroke="black" points="121.48,-43.4 113.8,-36.1 115.26,-46.6 121.48,-43.4"/>
+</g>
+</g>
+</svg>
diff --git a/doc/developer_resources.md b/doc/developer_resources.md
new file mode 100644
index 0000000..6d90a8b
--- /dev/null
+++ b/doc/developer_resources.md
@@ -0,0 +1,54 @@
+# Links and Resources for Emboss Developers
+
+## C++
+
+*   [ISO C++ drafts](https://github.com/cplusplus/draft/tree/main/papers)
+*   [C++14 final draft](https://github.com/cplusplus/draft/blob/main/papers/N3797.pdf)
+*   [C++17 initial draft (C++14 + minor fixes)](https://github.com/cplusplus/draft/blob/main/papers/n4140.pdf)
+*   [Final publicly-available drafts for each C++ revision](https://www.open-std.org/jtc1/sc22/wg21/docs/standards)
+*   [cppreference](https://en.cppreference.com/)
+*   [cplusplus.com reference](https://cplusplus.com/reference/)
+*   [Compiler Explorer (Godbolt)](https://godbolt.org/)
+
+
+### C++ Weirdness
+
+*   [Some background about `uint8_t` vs `unsigned char` from GCC archives](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66110#c10)
+*   [A bug story: data alignment on x86](https://pzemtsov.github.io/2016/11/06/bug-story-alignment-on-x86.html)
+*   [Casting unsigned to signed on OpenVMS HP C++ on Itanium](https://stackoverflow.com/questions/7601731/how-does-one-safely-static-cast-between-unsigned-int-and-int)
+
+
+## Python
+
+*   [Python official documentation, current](https://docs.python.org/3/)
+*   [Python official documentation, v3.9 (oldest non-EOL until October 2025)](https://docs.python.org/3.9/)
+*   [CPython lifecycle calendar](https://devguide.python.org/versions/)
+
+
+## Bit Manipulation Tricks
+
+*   [Sean Eron Anderson's Bit Twiddling Hacks page](https://graphics.stanford.edu/~seander/bithacks.html)
+    *   But be careful copying code snippets: some of the C code is either
+        non-portable or invokes undefined behavior.
+*   [*Hacker's Delight*, Second Edition, by Henry S. Warren, Jr. (book)](https://en.wikipedia.org/wiki/Hacker%27s_Delight)
+
+
+## Parsers
+
+*   ["On the translation of languages from left to right", Knuth, D., 1965][1]
+
+    [1]: https://doi.org/10.1016/S0019-9958(65)90426-2
+    *   The paper that introduced shift-reduce parsers, and the "Canonical LR"
+        table generation algorithm.
+*   ["Efficient Computation of LALR(1) Look-Ahead Sets", DeRemer, D. & Pennello, T., 1982](https://dl.acm.org/doi/pdf/10.1145/69622.357187)
+    *   The paper that introduced the LALR(1) table generation algorithm used
+        in Berkeley YACC and GNU Bison.
+*   ["Generating LR Syntax Error Messages from Examples", Jeffery, C., 2003](http://dx.doi.org/10.1145/937563.937566)
+    *   [Link to non-paywalled copy](https://www.cs.tufts.edu/~nr/cs257/archive/clinton-jefferey/lr-error-messages.pdf)
+    *   The paper that introduced the *Merr* error marking system.  Emboss uses
+        this algorithm for specifying parser errors.
+*   ["The IELR(1) algorithm for generating minimal LR(1) parser tables for non-LR(1) grammars with conflict resolution", Denny, J. & Malloy, B., 2010](https://doi.org/10.1016/j.scico.2009.08.001)
+    *   An algorithm for generating minimal parser tables for LR(1) languages.
+*   ["Practical LR Parser Generation", Zimmerman, J., 2022](https://doi.org/10.48550/arXiv.2209.08383)
+    *   Many improvements on LR(1) parsing.
+*   [*Compilers: Principles, Techniques, and Tools*, Second Edition, by Alfred V. Aho, Monica S. Lam, Ravi Sethi, and Jeffrey D. Ullman (the "Dragon Book")](https://en.wikipedia.org/wiki/Compilers:_Principles,_Techniques,_and_Tools)
diff --git a/doc/language-reference.md b/doc/language-reference.md
index d9e132a..db8682e 100644
--- a/doc/language-reference.md
+++ b/doc/language-reference.md
@@ -651,7 +651,7 @@
 ```
 
 
-##### `$size_in_bytes` {#size-in-bytes}
+##### `$size_in_bytes`
 
 An Emboss `struct` has an *intrinsic* size, which is the size required to hold
 every field in the `struct`, regardless of how many bytes are in the buffer that
@@ -702,7 +702,7 @@
 ```
 
 
-##### `$max_size_in_bytes` {#max-size-in-bytes}
+##### `$max_size_in_bytes`
 
 The `$max_size_in_bytes` virtual field is a constant value that is at least as
 large as the largest possible value for `$size_in_bytes`.  In most cases, it
@@ -720,7 +720,7 @@
 ```
 
 
-##### `$min_size_in_bytes` {#min-size-in-bytes}
+##### `$min_size_in_bytes`
 
 The `$min_size_in_bytes` virtual field is a constant value that is no larger
 than the smallest possible value for `$size_in_bytes`.  In most cases, it will
@@ -1004,7 +1004,7 @@
 ```
 
 
-##### `$size_in_bits` {#size-in-bits}
+##### `$size_in_bits`
 
 Like a `struct`, an Emboss `bits` has an *intrinsic* size, which is the size
 required to hold every field in the `bits`, regardless of how many bits are
@@ -1029,14 +1029,14 @@
 dynamic `$size_in_bits` fields.
 
 
-##### `$max_size_in_bits` {#max-size-in-bits}
+##### `$max_size_in_bits`
 
 Since `bits` must be fixed size, the `$max_size_in_bits` field has the same
 value as `$size_in_bits`.  It is provided for consistency with
 `$max_size_in_bytes`.
 
 
-##### `$min_size_in_bits` {#min-size-in-bits}
+##### `$min_size_in_bits`
 
 Since `bits` must be fixed size, the `$min_size_in_bits` field has the same
 value as `$size_in_bits`.  It is provided for consistency with
@@ -1178,15 +1178,15 @@
 binding):
 
 1.  `()` `$max()` `$present()` `$upper_bound()` `$lower_bound()`
-2.  unary `+` and `-` ([see note 1](#precedence-note-unary-plus-minus))
+2.  unary `+` and `-` ([see note 1](#note-1-unary-plusminus-precedence))
 3.  `*`
 4.  `+` `-`
-5.  `<` `>` `==` `!=` `>=` `<=` ([see note 2](#precedence-note-comparisons))
-6.  `&&` `||` ([see note 3](#precedence-note-and-or))
-7.  `?:` ([see note 4](#precedence-note-choice))
+5.  `<` `>` `==` `!=` `>=` `<=` ([see note 2](#note-2-chained-and-mixed-comparisons))
+6.  `&&` `||` ([see note 3](#note-3-logical-andor-precedence))
+7.  `?:` ([see note 4](#note-4-choice-operator-precedence))
 
 
-###### Note 1 {#precedence-note-unary-plus-minus}
+###### Note 1 (Unary Plus/Minus Precedence)
 
 Only one unary `+` or `-` may be applied to an expression without parentheses.
 These expressions are valid:
@@ -1207,7 +1207,7 @@
 ```
 
 
-###### Note 2 {#precedence-note-comparisons}
+###### Note 2 (Chained and Mixed Comparisons)
 
 The relational operators may be chained like so:
 
@@ -1240,7 +1240,7 @@
 Greater-than comparisons may not be mixed with less-than comparisons.
 
 
-###### Note 3 {#precedence-note-and-or}
+###### Note 3 (Logical And/Or Precedence)
 
 The boolean logical operators have the same precedence, but may not be mixed
 without parentheses.  The following are allowed:
@@ -1260,7 +1260,7 @@
 ```
 
 
-###### Note 4 {#precedence-note-choice}
+###### Note 4 (Choice Operator Precedence)
 
 The choice operator `?:` may not be chained without parentheses.  These are OK:
 
diff --git a/runtime/cpp/emboss_prelude.h b/runtime/cpp/emboss_prelude.h
index 8b5fa28..ad302cd 100644
--- a/runtime/cpp/emboss_prelude.h
+++ b/runtime/cpp/emboss_prelude.h
@@ -267,7 +267,7 @@
            static_cast</**/ ::std::uint64_t>(value) <=
                ((static_cast<ValueType>(1) << (Parameters::kBits - 1)) << 1) -
                    1 &&
-           Parameters::ValueIsOk(value);
+           Parameters::ValueIsOk(static_cast<ValueType>(value));
   }
   void UncheckedWrite(ValueType value) const {
     buffer_.UncheckedWriteUInt(value);
@@ -429,7 +429,8 @@
                     : ((static_cast<ValueType>(1) << (Parameters::kBits - 2)) -
                        1) * 2 +
                           1) &&
-           Parameters::ValueIsOk(value);
+           Parameters::ValueIsOk(static_cast<ValueType>(value));
+
   }
 
   void UncheckedWrite(ValueType value) const {
diff --git a/testdata/BUILD b/testdata/BUILD
index 923dcf3..f8a1be7 100644
--- a/testdata/BUILD
+++ b/testdata/BUILD
@@ -108,7 +108,7 @@
     srcs = [
         "enum.emb",
     ],
-    # This tag is arbitrary, and exists to ensure you can pass atributes common
+    # This tag is arbitrary, and exists to ensure you can pass attributes common
     # to all build rules to the underlying rules.
     tags = ["an_arbitrary_tag"],
 )