def add_type_indication(item: Union[str, MathObject], math_type: MathObject=None) -> Union[str, MathObject]: """ Add type indication for Lean. e.g. '0' -> (0:ℝ) 'x' -> (x:ℝ) :param item: either a string (provided by user in TextDialog) or MathObject :param math_type: math_type indication to add. If None, largest number set used in current context will be indicated :return: either string or MathObject, with type indication in name """ if math_type: number_type = math_type.which_number_set(is_math_type=True) if isinstance(item, str): number_set = which_number_set(item) if number_set and ':' not in item: if not math_type: MathObject.add_numbers_set(number_set) # Add type indication = largest set of numbers among used number_type = MathObject.number_sets[-1] item = f"({item}:{number_type})" # e.g. (0:ℝ) return item else: if not math_type: number_type = MathObject.number_sets[-1] if hasattr(item, 'info'): name = item.display_name # Do not put 2 type indications!! if (':' not in name and hasattr(item, 'info')): item.info['name'] = f"({name}:{number_type})" return item
async def solve_exercise(self): """ Launch exercise window, start lean server, and connect signals. """ log.debug(f"Starting exercise {self.exercise.pretty_name}") # Stop Lean server if running if self.servint: await self.servint.file_invalidated.wait() self.servint.stop() log.info("Lean server stopped!") # ─────────────────── Most Important ─────────────────── # # Re-initialisation of MathObject.Variables MathObject.clear() # Start Lean server self.servint = ServerInterface(self.nursery) await self.servint.start() log.info("Lean server started") # Start exercise window self.exercise_window = ExerciseMainWindow(self.exercise, self.servint) # Connect signals self.exercise_window.window_closed.connect(self.close_exercise_window) self.exercise_window.change_exercise.connect(self.choose_exercise) # Show window self.exercise_window.show()
def __init__(self, mathobject: MathObject, tag: str = '='): """ Init self with an instance of the class MathObject and a tag. See self.__doc__. :param mathobject: The MathObject one wants to display. :param tag: The tag of mathobject. """ super().__init__() self.mathobject = mathobject self.tag = tag lean_name = mathobject.to_display() math_expr = mathobject.math_type.to_display(is_math_type=True) caption = f'{lean_name} : {math_expr}' self.setText(caption) self.setIcon(_TagIcon(tag)) # set tool_tips (merge several tool_tips if needed) tool_tips = explain_how_to_apply(mathobject) # log.debug(f"Setting tooltips {tool_tips}") if len(tool_tips) == 1: tool_tip = _("Double click to") + " " + tool_tips[0] self.setToolTip(tool_tip) elif len(tool_tips) > 1: text = _("Double click to:") for tool_tip in tool_tips: text += "\n" + "• " + tool_tip self.setToolTip(text)
def action_forall(proof_step, selected_objects: [MathObject], user_input: [str] = [], target_selected: bool = True) -> CodeForLean: """ (1) If no selection and target is of the form ∀ x, P(x): introduce x and transform the target into P(x) (2) If a single universal property is selected, ask user for an object to which the property will be applied (3) If 2 or more items are selected, one of which is a universal property, try to apply it to the other selected items """ test_selection(selected_objects, target_selected) goal = proof_step.goal if len(selected_objects) == 0: if not goal.target.is_for_all(): error = _("target is not a universal property '∀x, P(x)'") raise WrongUserInput(error) else: return construct_forall(proof_step) elif len(selected_objects) == 1: # Ask user for item if not selected_objects[0].is_for_all(): error = _("selected property is not a universal property '∀x, " "P(x)'") raise WrongUserInput(error) elif not user_input: raise MissingParametersError(InputType.Text, title=_("Apply a universal property"), output=_("Enter element on which you " "want to apply:")) else: item = user_input[0] item = add_type_indication(item) # e.g. (0:ℝ) if item[0] != '(': item = '(' + item + ')' potential_var = MathObject(node="LOCAL_CONSTANT", info={'name': item}, children=[], math_type=None) selected_objects.insert(0, potential_var) # Now len(l) == 2 # From now on len(l) ≥ 2 # Search for a universal property among l, beginning with last item selected_objects.reverse() for item in selected_objects: if item.is_for_all(): # Put item on last position selected_objects.remove(item) selected_objects.reverse() selected_objects.append(item) return apply_forall(proof_step, selected_objects) raise WrongUserInput(error=_("no universal property among selected"))
def visit_expr(self, node, visited_children): """ Create MathObject from children (node, children, math_type if any, NO representation) """ node_info = visited_children[0][1] children = concatenate(visited_children[1:])[0] math_object = MathObject.from_info_and_children(info=node_info, children=children) return [math_object], {}
def inequality_from_pattern_matching(math_object: MathObject, variable: MathObject) -> MathObject: """ Check if math_object.math_type has the form ∀ x:X, (x R ... ==> ...) where R is some inequality relation, and if this statement may be applied to variable. If so, return inequality with x replaced by variable """ inequality = None if math_object.is_for_all(): math_type, var, body = math_object.math_type.children # NB: following line does not work because of coercions # if var.math_type == variable.math_type: if body.is_implication(is_math_type=True): premise = body.children[0] # children (2,0) if (premise.is_inequality(is_math_type=True) and var == premise.children[0]): children = [variable, premise.children[1]] inequality = MathObject(node=premise.node, info={}, children=children, math_type=premise.math_type) return inequality
def visit_ENTRY(self, node, visited_children): """ Create a global_var corresponding to the entry According to the rules, Entry has three children (head, sep_equal, tail) :return: """ # Get info from HEAD and a math_object from TAIL (_, head), _, ([tail], _) = visited_children info = head info['math_type'] = tail math_object = MathObject.from_info_and_children(info=info, children=[]) # log.debug(f"Creating global var MathObject {math_object}") return math_object
def construct_iff(proof_step, user_input: [str]) -> CodeForLean: """ Assuming target is an iff, split into two implications. """ target = proof_step.goal.target.math_type code = CodeForLean.empty_code() left = target.children[0] right = target.children[1] if not user_input: choices = [("⇒", f'({left.to_display()}) ⇒ ({right.to_display()})'), ("⇐", f'({right.to_display()}) ⇒ ({left.to_display()})')] raise MissingParametersError( InputType.Choice, choices, title=_("Choose sub-goal"), output=_("Which implication to prove first?")) elif len(user_input) == 1: if user_input[0] == 1: code = CodeForLean.from_string("rw iff.comm") left, right = right, left code = code.and_then("split") else: raise WrongUserInput(error=_("Undocumented error")) code.add_success_msg(_("Iff split in two implications")) impl1 = MathObject(info={}, node="PROP_IMPLIES", children=[left, right], math_type="PROP") impl2 = MathObject(info={}, node="PROP_IMPLIES", children=[right, left], math_type="PROP") code.add_conjunction(target, impl1, impl2) return code
def fireworks(self): """ As of now, - display a dialog when the target is successfully solved, - replace the target by a message "No more goal" Note that the dialog is displayed only the first time the signal is triggered, thanks to the flag self.cqdf. """ # TODO: make it a separate class # Display msg_box unless redoing or test mode # (Previously was: Display unless exercise already solved) # if not self.exercise_solved: if not self.proof_step.is_redo() and not self.test_mode: title = _('Target solved') text = _('The proof is complete!') msg_box = QMessageBox(parent=self) msg_box.setText(text) msg_box.setWindowTitle(title) button_ok = msg_box.addButton(_('Back to exercise'), QMessageBox.YesRole) button_change = msg_box.addButton(_('Change exercise'), QMessageBox.YesRole) button_change.clicked.connect(self.change_exercise) msg_box.exec() self.proof_step.no_more_goal = True self.proof_step.new_goals = [] # Artificially create a final proof_state by replacing target by a msg # (We do not get the final proof_state from Lean). proof_state = deepcopy(self.proof_step.proof_state) target = proof_state.goals[0].target target.math_type = MathObject( node="NO_MORE_GOAL", info={}, children=[], ) # Update proof_step and UI self.update_proof_state(proof_state) if not self.exercise_solved: self.exercise_solved = True if not self.test_mode: # Save exercise for auto-test self.lean_file.save_exercise_for_autotest(self)
def explain_how_to_apply(math_object: MathObject, dynamic=False, long=False) \ -> str: """ Provide explanations of how the math_object may be applied (--> to serve as tooltip or help) :param math_object: :param dynamic: if False, caption depends only on main node :param long: boolean TODO: implement dynamic and long tooltips """ captions = [] # default value if math_object.is_function(): captions.append(tooltips.get('tooltip_apply_function')) elif math_object.can_be_used_for_substitution()[0]: captions.append(tooltips.get('tooltip_apply_substitute')) return captions #TODO: include this when extended apply button # the following 4 cases are mutually exclusive if math_object.is_function(): captions.append(tooltips.get('tooltip_apply_function')) elif math_object.is_for_all(): captions.append(tooltips.get('tooltip_apply_for_all')) elif math_object.is_exists(): captions.append(tooltips.get('tooltip_apply_exists')) elif math_object.is_and(): captions.append(tooltips.get('tooltip_apply_and')) # Additional line if math_object.can_be_used_for_implication(): captions.append(tooltips.get('tooltip_apply_implication')) elif math_object.can_be_used_for_substitution()[0]: captions.append(tooltips.get('tooltip_apply_substitute')) return captions
def introduce_fun(proof_step, selected_objects: [MathObject]) -> CodeForLean: """ If a hypothesis of form ∀ a ∈ A, ∃ b ∈ B, P(a,b) has been previously selected: use the axiom of choice to introduce a new function f : A → B and add ∀ a ∈ A, P(a, f(a)) to the properties. """ goal = proof_step.goal error = _('select a property "∀ x, ∃ y, P(x,y)" to get a function') success = _("function {} and property {} added to the context") if len(selected_objects) == 1: h = selected_objects[0].info["name"] # Finding expected math_type for the new function universal_property = selected_objects[0] if universal_property.is_for_all(): source_type = universal_property.math_type.children[0] exist_property = universal_property.math_type.children[2] if exist_property.is_exists(is_math_type=True): target_type = exist_property.children[0] math_type = MathObject(node="FUNCTION", info={}, children=[source_type, target_type], math_type=NO_MATH_TYPE) hf = get_new_hyp(proof_step) f = give_global_name(math_type=math_type, proof_step=proof_step) code = CodeForLean.from_string(f'cases ' f'classical.axiom_of_choice ' f'{h} with {f} {hf}, ' f'dsimp at {hf}, ' f'dsimp at {f}') code.add_error_msg(error) success = success.format(f, hf) code.add_success_msg(success) return code raise WrongUserInput(error)
def give_local_name(math_type: MathObject, body: MathObject, hints: [str] = [], forbidden_vars: [MathObject] = []) -> str: """ Attribute a name to a local variable. See give_name below. Mainly computes all pertinent forbidden variables, by adding vars from body to the forbidden_vars list. :param math_type: MathObject type of new variable :param body: a MathObject inside which the new name will serve as a bound variable :param hints: a list of hints (str) for the future name :param forbidden_vars: list of vars (MathObject) whose names are forbidden :return: str, a name for the new variable """ additional_forbidden_vars = body.extract_local_vars() names = [var.info['name'] for var in forbidden_vars] # log.debug(f'Giving name to bound var, a priori forbidden names ={names}') more_names = [var.info['name'] for var in additional_forbidden_vars] # log.debug(f'Additional forbidden names ={more_names}') forbidden_vars.extend(additional_forbidden_vars) return give_name(math_type, forbidden_vars, hints)