def test_validated_memory_dict():
    memory = MemoryDict()
    memory_validator = ValidatedMemoryDict(memory=memory)

    def rule_identical_pairs(mem, addr):
        """
        Validates that the values in address pairs (i, i+1), where i is even, are identical.
        """
        offset_diff = (-1) ** (addr.offset % 2)
        other_addr = RelocatableValue.from_tuple((addr.segment_index, addr.offset + offset_diff))
        if other_addr in mem:
            assert mem[addr] == mem[other_addr]
            return {addr, other_addr}
        return set()

    def rule_constant_value(mem, addr, constant):
        assert mem[addr] == constant, \
            f'Expected value in address {addr} to be {constant}, got {mem[addr]}.'
        return {addr}

    memory_validator.add_validation_rule(1, lambda memory, addr: set())
    memory_validator.add_validation_rule(2, lambda memory, addr: {addr})
    memory_validator.add_validation_rule(3, rule_identical_pairs)
    memory_validator.add_validation_rule(4, rule_constant_value, 0)

    addr0 = RelocatableValue.from_tuple((1, 0))
    addr1 = RelocatableValue.from_tuple((2, 0))
    addr2 = RelocatableValue.from_tuple((3, 0))
    addr3 = RelocatableValue.from_tuple((3, 1))
    addr4 = RelocatableValue.from_tuple((4, 0))

    # Test validated_addresses update.
    memory_validator[addr0] = 0
    assert memory_validator._ValidatedMemoryDict__validated_addresses == set()
    memory_validator[addr1] = 0
    assert memory_validator._ValidatedMemoryDict__validated_addresses == {addr1}
    # Test validation rule application.
    memory_validator[addr2] = 1
    assert memory_validator._ValidatedMemoryDict__validated_addresses == {addr1}
    memory_validator[addr3] = 1
    assert memory_validator._ValidatedMemoryDict__validated_addresses == {addr1, addr2, addr3}

    with pytest.raises(
            AssertionError, match='Expected value in address 4:0 to be 0, got 1.'):
        memory_validator[addr4] = 1
Exemplo n.º 2
0
    def __init__(
            self, program: ProgramBase, run_context: RunContext,
            hint_locals: dict, static_locals: dict = {},
            builtin_runners: Dict[str, BuiltinRunner] = {}, program_base: Optional[int] = None):
        """
        hints - a dictionary from memory addresses to an executable object.
          When the pc points to the memory address, before the execution of the instruction,
          the executable object will be run.
          Executable objects are anything that can be placed inside exec.
          For example, 'a=5', or compile('a=5').
        hint_locals - dictionary holding local values for execution of hints.
          Passed as locals parameter for the exec function.
        static_locals - dictionary holding static values for execution. They are available in all
          scopes.
        program_base - The pc of the first instruction in program (default is run_context.pc).
        """
        self.prime = program.prime
        self.builtin_runners = builtin_runners
        self.exec_scopes: List[dict] = []
        self.enter_scope(dict(hint_locals))
        self.run_context = copy.copy(run_context)  # Shallow copy.
        self.hints: Dict[MaybeRelocatable, CompiledHint] = {}
        # A map from hint index to pc.
        self.hint_pcs: Dict[int, MaybeRelocatable] = {}
        self.instruction_debug_info: Dict[MaybeRelocatable, InstructionLocation] = {}
        self.debug_file_contents: Dict[str, str] = {}
        self.program = program
        self.program_base = program_base if program_base is not None else self.run_context.pc
        self.validated_memory = ValidatedMemoryDict(memory=self.run_context.memory)

        # If program is a StrippedProgram, there are no hints or debug information to load.
        if isinstance(program, Program):
            self.load_program(
                program=program,
                program_base=self.program_base,
            )

        self.trace: List[TraceEntry[MaybeRelocatable]] = []

        # auto_deduction contains a mapping from a memory segment index to a list of functions
        # (and a tuple of additional arguments) that may try to automatically deduce the value
        # of memory cells in the segment (based on other memory cells).
        self.auto_deduction: Dict[int, List[Tuple[Rule, tuple]]] = {}
        # Current step.
        self.current_step = 0

        # This flag can be set to true by hints to avoid the execution of the current step in
        # step() (so that only the hint will be performed, but nothing else will happen).
        self.skip_instruction_execution = False

        from starkware.python import math_utils
        self.static_locals = static_locals.copy()
        self.static_locals.update({
            'PRIME': self.prime,
            'fadd': lambda a, b, p=self.prime: (a + b) % p,
            'fsub': lambda a, b, p=self.prime: (a - b) % p,
            'fmul': lambda a, b, p=self.prime: (a * b) % p,
            'fdiv': lambda a, b, p=self.prime: math_utils.div_mod(a, b, p),
            'fpow': lambda a, b, p=self.prime: pow(a, b, p),
            'fis_quad_residue': lambda a, p=self.prime: math_utils.is_quad_residue(a, p),
            'fsqrt': lambda a, p=self.prime: math_utils.sqrt(a, p),
            'safe_div': math_utils.safe_div,
        })
Exemplo n.º 3
0
class VirtualMachine:
    def __init__(
            self, program: ProgramBase, run_context: RunContext,
            hint_locals: dict, static_locals: dict = {},
            builtin_runners: Dict[str, BuiltinRunner] = {}, program_base: Optional[int] = None):
        """
        hints - a dictionary from memory addresses to an executable object.
          When the pc points to the memory address, before the execution of the instruction,
          the executable object will be run.
          Executable objects are anything that can be placed inside exec.
          For example, 'a=5', or compile('a=5').
        hint_locals - dictionary holding local values for execution of hints.
          Passed as locals parameter for the exec function.
        static_locals - dictionary holding static values for execution. They are available in all
          scopes.
        program_base - The pc of the first instruction in program (default is run_context.pc).
        """
        self.prime = program.prime
        self.builtin_runners = builtin_runners
        self.exec_scopes: List[dict] = []
        self.enter_scope(dict(hint_locals))
        self.run_context = copy.copy(run_context)  # Shallow copy.
        self.hints: Dict[MaybeRelocatable, CompiledHint] = {}
        # A map from hint index to pc.
        self.hint_pcs: Dict[int, MaybeRelocatable] = {}
        self.instruction_debug_info: Dict[MaybeRelocatable, InstructionLocation] = {}
        self.debug_file_contents: Dict[str, str] = {}
        self.program = program
        self.program_base = program_base if program_base is not None else self.run_context.pc
        self.validated_memory = ValidatedMemoryDict(memory=self.run_context.memory)

        # If program is a StrippedProgram, there are no hints or debug information to load.
        if isinstance(program, Program):
            self.load_program(
                program=program,
                program_base=self.program_base,
            )

        self.trace: List[TraceEntry[MaybeRelocatable]] = []

        # auto_deduction contains a mapping from a memory segment index to a list of functions
        # (and a tuple of additional arguments) that may try to automatically deduce the value
        # of memory cells in the segment (based on other memory cells).
        self.auto_deduction: Dict[int, List[Tuple[Rule, tuple]]] = {}
        # Current step.
        self.current_step = 0

        # This flag can be set to true by hints to avoid the execution of the current step in
        # step() (so that only the hint will be performed, but nothing else will happen).
        self.skip_instruction_execution = False

        from starkware.python import math_utils
        self.static_locals = static_locals.copy()
        self.static_locals.update({
            'PRIME': self.prime,
            'fadd': lambda a, b, p=self.prime: (a + b) % p,
            'fsub': lambda a, b, p=self.prime: (a - b) % p,
            'fmul': lambda a, b, p=self.prime: (a * b) % p,
            'fdiv': lambda a, b, p=self.prime: math_utils.div_mod(a, b, p),
            'fpow': lambda a, b, p=self.prime: pow(a, b, p),
            'fis_quad_residue': lambda a, p=self.prime: math_utils.is_quad_residue(a, p),
            'fsqrt': lambda a, p=self.prime: math_utils.sqrt(a, p),
            'safe_div': math_utils.safe_div,
        })

    def validate_existing_memory(self):
        """
        Validates the builtin values (e.g., range-checks) that are already written to the VM's
        memory.
        """
        self.validated_memory.validate_existing_memory()

    def load_hints(self, program: Program, program_base: MaybeRelocatable):
        for i, (pc, hint) in enumerate(program.hints.items(), len(self.hint_pcs)):
            self.hints[pc + program_base] = CompiledHint(
                compiled=self.compile_hint(hint.code, f'<hint{i}>'),
                # Use hint=hint in the lambda's arguments to capture this value (otherwise, it
                # will use the same hint object for all iterations).
                consts=lambda pc, ap, fp, memory, hint=hint: VmConsts(
                    context=VmConstsContext(
                        identifiers=program.identifiers,
                        evaluator=ExpressionEvaluator(self.prime, ap, fp, memory).eval,
                        reference_manager=program.reference_manager,
                        flow_tracking_data=hint.flow_tracking_data,
                        memory=memory,
                        pc=pc),
                    accessible_scopes=hint.accessible_scopes))
            self.hint_pcs[i] = pc + program_base

    def load_debug_info(self, debug_info: Optional[DebugInfo], program_base: MaybeRelocatable):
        if debug_info is None:
            return

        self.debug_file_contents.update(debug_info.file_contents)

        for offset, location_info in debug_info.instruction_locations.items():
            self.instruction_debug_info[program_base + offset] = location_info

    def load_program(self, program: Program, program_base: MaybeRelocatable):
        assert self.prime == program.prime, \
            f'Unexpected prime for loaded program: {program.prime} != {self.prime}.'

        self.load_hints(program, program_base)
        self.load_debug_info(program.debug_info, program_base)

    def enter_scope(self, new_scope_locals: Optional[dict] = None):
        """
        Starts a new scope of user-defined local variables available to hints.
        Note that variables defined in outer scopes will not be available in the new scope.
        A dictionary of locals that should be available in the new scope should be passed in
        new_scope_locals.
        The scope starts only from the next hint.
        exit_scope() must be called to resume the previous scope.
        """
        if new_scope_locals is None:
            new_scope_locals = {}

        self.exec_scopes.append({**new_scope_locals, **self.builtin_runners})

    def exit_scope(self):
        self.exec_scopes.pop()

    def update_registers(self, instruction: Instruction, operands: Operands):
        # Update fp.
        if instruction.fp_update is Instruction.FpUpdate.AP_PLUS2:
            self.run_context.fp = self.run_context.ap + 2
        elif instruction.fp_update is Instruction.FpUpdate.DST:
            self.run_context.fp = operands.dst
        elif instruction.fp_update is not Instruction.FpUpdate.REGULAR:
            raise NotImplementedError('Invalid fp_update value')

        # Update ap.
        if instruction.ap_update is Instruction.ApUpdate.ADD:
            if operands.res is None:
                raise NotImplementedError('Res.UNCONSTRAINED cannot be used with ApUpdate.ADD')
            self.run_context.ap += operands.res % self.prime
        elif instruction.ap_update is Instruction.ApUpdate.ADD1:
            self.run_context.ap += 1
        elif instruction.ap_update is Instruction.ApUpdate.ADD2:
            self.run_context.ap += 2
        elif instruction.ap_update is not Instruction.ApUpdate.REGULAR:
            raise NotImplementedError('Invalid ap_update value')
        self.run_context.ap = self.run_context.ap % self.prime

        # Update pc.
        # The pc update should be done last so that we will have the correct pc in case of an
        # exception during one of the updates above.
        if instruction.pc_update is Instruction.PcUpdate.REGULAR:
            self.run_context.pc += instruction.size
        elif instruction.pc_update is Instruction.PcUpdate.JUMP:
            if operands.res is None:
                raise NotImplementedError('Res.UNCONSTRAINED cannot be used with PcUpdate.JUMP')
            self.run_context.pc = operands.res
        elif instruction.pc_update is Instruction.PcUpdate.JUMP_REL:
            if operands.res is None:
                raise NotImplementedError('Res.UNCONSTRAINED cannot be used with PcUpdate.JUMP_REL')
            if not isinstance(operands.res, int):
                raise PureValueError('jmp rel', operands.res)
            self.run_context.pc += operands.res
        elif instruction.pc_update is Instruction.PcUpdate.JNZ:
            if self.is_zero(operands.dst):
                self.run_context.pc += instruction.size
            else:
                self.run_context.pc += operands.op1
        else:
            raise NotImplementedError('Invalid pc_update value')
        self.run_context.pc = self.run_context.pc % self.prime

    def deduce_op0(
            self, instruction: Instruction, dst: Optional[MaybeRelocatable],
            op1: Optional[MaybeRelocatable]) -> \
            Tuple[Optional[MaybeRelocatable], Optional[MaybeRelocatable]]:
        if instruction.opcode is Instruction.Opcode.CALL:
            return self.run_context.pc + instruction.size, None
        elif instruction.opcode is Instruction.Opcode.ASSERT_EQ:
            if (instruction.res is Instruction.Res.ADD) and (dst is not None) and \
                    (op1 is not None):
                return (dst - op1) % self.prime, dst  # type: ignore
            elif (instruction.res is Instruction.Res.MUL) and isinstance(dst, int) and \
                    isinstance(op1, int) and op1 != 0:
                return div_mod(dst, op1, self.prime), dst
        return None, None

    def deduce_op1(
            self, instruction: Instruction, dst: Optional[MaybeRelocatable],
            op0: Optional[MaybeRelocatable]) -> \
            Tuple[Optional[MaybeRelocatable], Optional[MaybeRelocatable]]:
        if instruction.opcode is Instruction.Opcode.ASSERT_EQ:
            if (instruction.res is Instruction.Res.OP1) and (dst is not None):
                return dst, dst
            elif (instruction.res is Instruction.Res.ADD) and (dst is not None) and \
                    (op0 is not None):
                return (dst - op0) % self.prime, dst  # type: ignore
            elif (instruction.res is Instruction.Res.MUL) and isinstance(dst, int) and \
                    isinstance(op0, int) and op0 != 0:
                return div_mod(dst, op0, self.prime), dst
        return None, None

    def compute_res(
            self, instruction: Instruction, op0: MaybeRelocatable, op1: MaybeRelocatable,
            op0_addr: MaybeRelocatable) -> Optional[MaybeRelocatable]:
        if instruction.res is Instruction.Res.OP1:
            return op1
        elif instruction.res is Instruction.Res.ADD:
            return (op0 + op1) % self.prime
        elif instruction.res is Instruction.Res.MUL:
            if isinstance(op0, RelocatableValue) or isinstance(op1, RelocatableValue):
                raise PureValueError('*', op0, op1)
            return (op0 * op1) % self.prime
        elif instruction.res is Instruction.Res.UNCONSTRAINED:
            # In this case res should be the inverse of dst.
            # For efficiency, we do not compute it here.
            return None
        else:
            raise NotImplementedError('Invalid res value')

    def compute_operands(self, instruction: Instruction) -> \
            Tuple[Operands, List[int], List[MaybeRelocatable]]:
        """
        Computes the values of the operands. Deduces dst if needed.
        Returns:
          operands - an Operands instance with the values of the operands.
          mem_addresses - the memory addresses for the 3 memory units used (dst, op0, op1).
          mem_values - the corresponding memory values.
        """
        # Try to fetch dst, op0, op1.
        # op0 throughout this function represents the value at op0_addr.
        # If op0 is set, this implies that we are going to set memory at op0_addr to that value.
        # Same for op1, dst.
        dst_addr = self.run_context.compute_dst_addr(instruction)
        dst: Optional[MaybeRelocatable] = self.validated_memory.get(dst_addr)
        op0_addr = self.run_context.compute_op0_addr(instruction)
        op0: Optional[MaybeRelocatable] = self.validated_memory.get(op0_addr)
        op1_addr = self.run_context.compute_op1_addr(instruction, op0=op0)
        op1: Optional[MaybeRelocatable] = self.validated_memory.get(op1_addr)
        # res throughout this function represents the computation on op0,op1
        # as defined in decode.py.
        # If it is set, this implies that compute_res(...) will return this value.
        # If it is set without invoking compute_res(), this is an optimization, but should not
        # yield a different result.
        # In particular, res may be different than dst, even in ASSERT_EQ. In this case,
        # The ASSERT_EQ validation will fail in opcode_assertions().
        res: Optional[MaybeRelocatable] = None

        # Auto deduction rules.
        # Note: This may fail to deduce if 2 auto deduction rules are needed to be used in
        # a different order.
        if op0 is None:
            op0 = self.deduce_memory_cell(op0_addr)
        if op1 is None:
            op1 = self.deduce_memory_cell(op1_addr)

        should_update_dst = dst is None
        should_update_op0 = op0 is None
        should_update_op1 = op1 is None

        # Deduce op0 if needed.
        if op0 is None:
            op0, deduced_res = self.deduce_op0(instruction, dst, op1)
            if res is None:
                res = deduced_res

        # Deduce op1 if needed.
        if op1 is None:
            op1, deduced_res = self.deduce_op1(instruction, dst, op0)
            if res is None:
                res = deduced_res

        # Force pulling op0, op1 from memory for soundness test
        # and to get an informative error message if they were not computed.
        if op0 is None:
            op0 = self.validated_memory[op0_addr]
        if op1 is None:
            op1 = self.validated_memory[op1_addr]

        # Compute res if needed.
        if res is None:
            res = self.compute_res(instruction, op0, op1, op0_addr)

        # Deduce dst.
        if dst is None:
            if instruction.opcode is Instruction.Opcode.ASSERT_EQ and res is not None:
                dst = res
            elif instruction.opcode is Instruction.Opcode.CALL:
                dst = self.run_context.fp

        # Force pulling dst from memory for soundness.
        if dst is None:
            dst = self.validated_memory[dst_addr]

        # Write updated values.
        if should_update_dst:
            self.validated_memory[dst_addr] = dst
        if should_update_op0:
            self.validated_memory[op0_addr] = op0
        if should_update_op1:
            self.validated_memory[op1_addr] = op1

        return Operands(
            dst=dst,
            op0=op0,
            op1=op1,
            res=res), [dst_addr, op0_addr, op1_addr], [dst, op0, op1]

    def is_zero(self, value):
        """
        Returns True if value is zero (used for jnz instructions).
        This function can be overridden by subclasses.
        """
        if not isinstance(value, int):
            raise PureValueError('jmp != 0', value)
        return value == 0

    def is_integer_value(self, value):
        """
        Returns True if value is integer rather than relocatable.
        This function can be overridden by subclasses.
        """
        return isinstance(value, int)

    @staticmethod
    @lru_cache(None)
    def decode_instruction(encoded_inst: int, imm: Optional[int] = None):
        return decode_instruction(encoded_inst, imm)

    def decode_current_instruction(self):
        try:
            instruction_encoding, imm = self.run_context.get_instruction_encoding()
        except Exception as exc:
            raise self.as_vm_exception(exc) from None

        instruction = self.decode_instruction(instruction_encoding, imm)

        return instruction, instruction_encoding

    def opcode_assertions(self, instruction: Instruction, operands: Operands):
        if instruction.opcode is Instruction.Opcode.ASSERT_EQ:
            if operands.res is None:
                raise NotImplementedError(
                    'Res.UNCONSTRAINED cannot be used with Opcode.ASSERT_EQ')
            if operands.dst != operands.res and not self.check_eq(operands.dst, operands.res):
                raise Exception(
                    f'An ASSERT_EQ instruction failed: {operands.dst} != {operands.res}')
        elif instruction.opcode is Instruction.Opcode.CALL:
            next_pc = self.run_context.pc + instruction.size
            if operands.op0 != next_pc and not self.check_eq(operands.op0, next_pc):
                raise Exception(
                    'Call failed to write return-pc (inconsistent op0): ' +
                    f'{operands.op0} != {next_pc}. Did you forget to increment ap?')
            fp = self.run_context.fp
            if operands.dst != fp and not self.check_eq(operands.dst, fp):
                raise Exception(
                    'Call failed to write return-fp (inconsistent dst): ' +
                    f'{operands.dst} != {fp}. Did you forget to increment ap?')
        elif instruction.opcode in [Instruction.Opcode.RET, Instruction.Opcode.NOP]:
            # Nothing to check.
            pass
        else:
            raise NotImplementedError(f'Unsupported opcode {instruction.opcode}')

    def step(self):
        self.skip_instruction_execution = False
        # Execute hints.
        hint = self.hints.get(self.run_context.pc)

        if hint is not None:
            exec_locals = self.exec_scopes[-1]
            exec_locals['memory'] = memory = self.validated_memory
            exec_locals['ap'] = ap = self.run_context.ap
            exec_locals['fp'] = fp = self.run_context.fp
            exec_locals['pc'] = pc = self.run_context.pc
            exec_locals['current_step'] = self.current_step
            exec_locals['ids'] = hint.consts(pc, ap, fp, memory)

            exec_locals['vm_load_program'] = self.load_program
            exec_locals['vm_enter_scope'] = self.enter_scope
            exec_locals['vm_exit_scope'] = self.exit_scope
            exec_locals.update(self.static_locals)

            self.exec_hint(hint.compiled, exec_locals)

            # Clear ids (which will be rewritten by the next hint anyway) to make the VM instance
            # smaller and faster to copy.
            del exec_locals['ids']
            del exec_locals['memory']

            if self.skip_instruction_execution:
                return

        # Decode.
        instruction, instruction_encoding = self.decode_current_instruction()

        self.run_instruction(instruction, instruction_encoding)

    def compile_hint(self, source, filename):
        """
        Compiles the given python source code.
        This function can be overridden by subclasses.
        """
        return compile(source, filename, mode='exec')

    def exec_hint(self, code, globals_):
        """
        Executes the given code with the given globals.
        This function can be overridden by subclasses.
        """
        try:
            exec(code, globals_)
        except Exception:
            hint_exception = HintException(self, *sys.exc_info())
            raise self.as_vm_exception(
                hint_exception, notes=[hint_exception.exception_str], hint=True) from None

    def run_instruction(self, instruction, instruction_encoding):
        try:
            # Compute operands.
            operands, operands_mem_addresses, operands_mem_values = self.compute_operands(
                instruction)
        except Exception as exc:
            raise self.as_vm_exception(exc) from None

        try:
            # Opcode assertions.
            self.opcode_assertions(instruction, operands)
        except Exception as exc:
            raise self.as_vm_exception(exc) from None

        # Write to trace.
        self.trace.append(TraceEntry(
            pc=self.run_context.pc,
            ap=self.run_context.ap,
            fp=self.run_context.fp,
        ))

        try:
            # Update registers.
            self.update_registers(instruction, operands)
        except Exception as exc:
            raise self.as_vm_exception(exc) from None

        self.current_step += 1

    def check_eq(self, val0, val1):
        """
        Called when an instruction encounters an assertion that two values should be equal.
        This function can be overridden by subclasses.
        """
        return val0 == val1

    @property
    def last_pc(self):
        """
        Returns the value of the program counter for the last instruction that was execute.
        Note that this is different from self.run_context.pc which contains the value of the
        next instruction to be executed.
        """
        return self.trace[-1].pc

    def as_vm_exception(self, exc, pc=None, notes: Optional[List[str]] = None, hint: bool = False):
        """
        Wraps the exception with a VmException, adding to it location information. If pc is not
        given the current pc is used.
        """
        traceback = None
        if pc is None:
            pc = self.run_context.pc
            traceback = self.get_traceback()

        return VmException(
            pc=pc,
            inst_location=self.get_location(pc=pc),
            inner_exc=exc,
            traceback=traceback,
            notes=notes,
            hint=hint,
        )

    def get_location(self, pc) -> Optional[InstructionLocation]:
        return self.instruction_debug_info.get(pc)

    def get_traceback(self) -> Optional[str]:
        """
        Returns the traceback at the current pc.
        """
        traceback = ''
        for traceback_pc in self.run_context.get_traceback_entries():
            location = self.get_location(pc=traceback_pc)
            if location is None:
                traceback += f'Unknown location (pc={traceback_pc})\n'
                continue
            traceback += location.inst.to_string_with_content(message=f'(pc={traceback_pc})') + '\n'
        if len(traceback) == 0:
            return None
        return 'Cairo traceback (most recent call last):\n' + traceback

    def add_validation_rule(self, segment_index, rule: ValidationRule, *args):
        self.validated_memory.add_validation_rule(segment_index, rule, *args)

    def add_auto_deduction_rule(self, segment_index, rule: Rule, *args):
        """
        Adds an auto deduction rule for the given memory segment.
        'rule' will be called with an address of a memory cell. It may return a value for the
        memory cell or None if the auto deduction does not apply.
        """
        self.auto_deduction.setdefault(segment_index, []).append((rule, args))

    def deduce_memory_cell(self, addr) -> Optional[MaybeRelocatable]:
        """
        Tries to deduce the value of memory[addr] if it was not already computed.
        Returns the value if deduced, otherwise returns None.
        """
        if not isinstance(addr, RelocatableValue):
            return None

        rules = self.auto_deduction.get(addr.segment_index, [])
        for rule, args in rules:
            value = rule(self, addr, *args)
            if value is None:
                continue

            self.validated_memory[addr] = value
            return value
        return None

    def verify_auto_deductions(self):
        """
        Makes sure that all assigned memory cells are consistent with their auto deduction rules.
        """
        for addr in self.validated_memory:
            if not isinstance(addr, RelocatableValue):
                continue
            for rule, args in self.auto_deduction.get(addr.segment_index, []):
                value = rule(self, addr, *args)
                if value is None:
                    continue

                current = self.validated_memory[addr]
                # If the values are not the same, try using check_eq to allow a subclass
                # to override this result.
                if current != value and not self.check_eq(current, value):
                    raise InconsistentAutoDeductionError(addr, current, value)

    def end_run(self):
        self.verify_auto_deductions()
        assert len(self.exec_scopes) == 1, \
            'Every enter_scope() requires a corresponding exit_scope().'