diff --git a/CHANGES.rst b/CHANGES.rst index d763134ae..6f51e8ac4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ .. contents:: CHANGES -======= +******* 5.0.3dev0 --------- @@ -42,14 +42,17 @@ Internals #. ``eval*`` methods in `Builtin` classes are considerer as synonyms of ``apply*`` methods. #. Modularize and improve the way in which `Builtin` classes are selected to have an associated `Definition`. #. `_SetOperator.assign_elementary` was renamed as `_SetOperator.assign`. All the special cases are not handled by the `_SetOperator.special_cases` dict. - - +#. ``get_sort_key`` now has a uniform interface on all the subclasses of ``KeyComparable``, accepting the parameter `pattern_sort`. +#. circular dependencies in ``mathics.core.definitions`` were reduced. +#. `to_boxes` now returns always a `FullForm` of the input Expression, instead of raising an exception. Bugs ++++ # ``0`` with a given precision (like in ```0`3```) is now parsed as ``0``, an integer number. #. ``RandomSample`` with one list argument now returns a random ordering of the list items. Previously it would return just one item. +#. Rules of the form ``pat->Condition[expr, cond]`` are handled as in WL. The same works for nested `Condition` expressions. Also, comparisons among these rules was improved. +#. ``mathics.core.Pattern.create`` now catches the case when the input parameter is not an `Atom` or an `Expression`. Enhancements diff --git a/mathics/builtin/assignments/assignment.py b/mathics/builtin/assignments/assignment.py index 7f3008bf1..5dcf0fca7 100644 --- a/mathics/builtin/assignments/assignment.py +++ b/mathics/builtin/assignments/assignment.py @@ -5,8 +5,8 @@ from mathics.builtin.base import BinaryOperator, Builtin -from mathics.core.assignment import ( - ASSIGNMENT_FUNCTION_MAP, +from mathics.core.eval.set import ( + SET_EVAL_FUNCTION_MAP, AssignmentException, assign_store_rules_by_tag, normalize_lhs, @@ -47,7 +47,7 @@ def assign(self, lhs, rhs, evaluation, tags=None, upset=False): try: # Using a builtin name, find which assignment procedure to perform, # and then call that function. - assignment_func = ASSIGNMENT_FUNCTION_MAP.get(lookup_name, None) + assignment_func = SET_EVAL_FUNCTION_MAP.get(lookup_name, None) if assignment_func: return assignment_func(self, lhs, rhs, evaluation, tags, upset) @@ -170,11 +170,21 @@ class SetDelayed(Set): 'Condition' ('/;') can be used with 'SetDelayed' to make an assignment that only holds if a condition is satisfied: >> f[x_] := p[x] /; x>0 + >> f[x_] := p[-x]/; x<-2 >> f[3] = p[3] >> f[-3] - = f[-3] - It also works if the condition is set in the LHS: + = p[3] + >> f[-1] + = f[-1] + Notice that the LHS is the same in both definitions, but the second + does not overwrite the first one. + + To overwrite one of these definitions, we have to assign using the same condition: + >> f[x_] := Sin[x] /; x>0 + >> f[3] + = Sin[3] + In a similar way, the condition can be set in the LHS: >> F[x_, y_] /; x < y /; x>0 := x / y; >> F[x_, y_] := y / x; >> F[2, 3] diff --git a/mathics/builtin/base.py b/mathics/builtin/base.py index b6faf3aac..4a2fd7b92 100644 --- a/mathics/builtin/base.py +++ b/mathics/builtin/base.py @@ -818,7 +818,7 @@ def get_head_name(self): def get_lookup_name(self): return self.get_name() - def get_sort_key(self) -> tuple: + def get_sort_key(self, pattern_sort=False) -> tuple: return self.to_expression().get_sort_key() def get_string_value(self): @@ -826,7 +826,12 @@ def get_string_value(self): def sameQ(self, expr) -> bool: """Mathics SameQ""" - return expr.sameQ(self) + if expr is self: + return True + # Otherwise, we need to convert + # self into an to expression + # to avoid an infinite loop... + return self.to_expression().sameQ(expr) def do_format(self, evaluation, format): return self diff --git a/mathics/builtin/box/layout.py b/mathics/builtin/box/layout.py index f9e4a7401..b25956c3d 100644 --- a/mathics/builtin/box/layout.py +++ b/mathics/builtin/box/layout.py @@ -23,6 +23,7 @@ from mathics.core.symbols import Symbol, SymbolMakeBoxes from mathics.core.systemsymbols import ( SymbolFractionBox, + SymbolFullForm, SymbolRowBox, SymbolSqrtBox, SymbolStandardForm, @@ -58,7 +59,12 @@ def to_boxes(x, evaluation: Evaluation, options={}) -> BoxElementMixin: return x_boxed if isinstance(x_boxed, Atom): return to_boxes(x_boxed, evaluation, options) - raise Exception(x, "cannot be boxed.") + + return RowBox( + String("BoxError: <<"), + to_boxes(Expression(SymbolFullForm, x), evaluation, options), + String(">>"), + ) class BoxData(Builtin): @@ -201,7 +207,10 @@ class RowBox(BoxExpression): summary_text = "horizontal arrange of boxes" def __repr__(self): - return "RowBox[List[" + self.items.__repr__() + "]]" + try: + return "RowBox[List[" + self.items.__repr__() + "]]" + except: + return "RowBox[List[{uninitialized}]]" def apply_list(self, boxes, evaluation): """RowBox[boxes_List]""" diff --git a/mathics/builtin/patterns.py b/mathics/builtin/patterns.py index 72fae8e61..b9df63681 100644 --- a/mathics/builtin/patterns.py +++ b/mathics/builtin/patterns.py @@ -172,7 +172,8 @@ def create_rules(rules_expr, expr, name, evaluation, extra_args=[]): else: result = [] for rule in rules: - if rule.get_head_name() not in ("System`Rule", "System`RuleDelayed"): + head_name = rule.get_head_name() + if head_name not in ("System`Rule", "System`RuleDelayed"): evaluation.message(name, "reps", rule) return None, True elif len(rule.elements) != 2: @@ -186,7 +187,13 @@ def create_rules(rules_expr, expr, name, evaluation, extra_args=[]): ) return None, True else: - result.append(Rule(rule.elements[0], rule.elements[1])) + result.append( + Rule( + rule.elements[0], + rule.elements[1], + delayed=(head_name == "System`RuleDelayed"), + ) + ) return result, False @@ -1690,7 +1697,7 @@ def __init__(self, rulelist, evaluation): self._elements = None self._head = SymbolDispatch - def get_sort_key(self) -> tuple: + def get_sort_key(self, pattern_sort=False) -> tuple: return self.src.get_sort_key() def get_atom_name(self): diff --git a/mathics/core/assignment.py b/mathics/core/assignment.py index 90ca57cb9..21ea601a9 100644 --- a/mathics/core/assignment.py +++ b/mathics/core/assignment.py @@ -5,41 +5,23 @@ from typing import Optional, Tuple -from mathics.algorithm.parts import walk_parts -from mathics.core.atoms import Atom, Integer +from mathics.core.atoms import Atom +from mathics.core.attributes import A_PROTECTED from mathics.core.element import BaseElement -from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit -from mathics.core.expression import Expression, SymbolDefault +from mathics.core.expression import Expression from mathics.core.list import ListExpression +from mathics.core.pattern import Pattern from mathics.core.rules import Rule -from mathics.core.symbols import ( - Symbol, - SymbolFalse, - SymbolList, - SymbolMinPrecision, - SymbolMaxPrecision, - SymbolN, - SymbolTrue, - valid_context_name, -) +from mathics.core.symbols import Symbol + from mathics.core.systemsymbols import ( SymbolAnd, - SymbolBlank, SymbolCondition, SymbolHoldPattern, - SymbolMachinePrecision, - SymbolOptionValue, - SymbolPart, - SymbolPattern, SymbolRuleDelayed, ) -from mathics.core.attributes import attribute_string_to_number, A_LOCKED, A_PROTECTED - -from functools import reduce - - class AssignmentException(Exception): def __init__(self, lhs, rhs) -> None: super().__init__(" %s cannot be assigned to %s" % (rhs, lhs)) @@ -47,43 +29,66 @@ def __init__(self, lhs, rhs) -> None: self.rhs = rhs -def assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset=None): - """ - This is the default assignment. Stores a rule of the form lhs->rhs - as a value associated to each symbol listed in tags. - For special cases, such like conditions or patterns in the lhs, - lhs and rhs are rewritten in a normal form, where - conditions are associated to the lhs. - """ - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - defs = evaluation.definitions - ignore_protection, tags = process_assign_other( - self, lhs, rhs, evaluation, tags, upset - ) - # In WMA, this does not happens. However, if we remove this, - # some combinatorica tests fail. - # Also, should not be at the begining? - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - count = 0 - rule = Rule(lhs, rhs) - position = "up" if upset else None - for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation, ignore_protection): - continue - count += 1 - defs.add_rule(tag, rule, position=position) - return count > 0 +def find_focus(focus): + """ + Recursively, look for the (true) focus expression, i.e., + the expression after strip it from Condition, Pattern and HoldPattern + wrapping expressions. + """ + name = focus.get_lookup_name() + if isinstance(focus, Pattern): + return find_focus(focus.expr) + if name == "System`HoldPattern": + if len(focus.elements) == 1: + return find_focus(focus.elements[0]) + # If HoldPattern appears with more than one element, + # the message + # "HoldPattern::argx: HoldPattern called with 2 arguments; 1 argument is expected." + # must be shown. + raise AssignmentException(focus, None) + if focus.has_form("System`Condition", 2): + return find_focus(focus.elements[0]) + if name == "System`Optional": + return None + if name == "System`Pattern" and len(focus.elements) == 2: + pat = focus.elements[1] + if pat.get_head_name() in ( + "System`Blank", + "System`BlankSequence", + "System`BlankNullSequence", + ): + elems = pat.elements + if len(elems) == 0: + return None + return find_focus(elems[0]) + else: + return find_focus(pat) + return focus + +def __delete_me_find_tag(focus): + focus = find_focus(focus) + if focus is None: + return None + return focus.get_lookup_name() -def build_rulopc(optval): - return Rule( - Expression( - SymbolOptionValue, - Expression(SymbolPattern, Symbol("$cond$"), SymbolBlank), - ), - Expression(SymbolOptionValue, optval, Symbol("$cond$")), - ) + +def find_tag_and_check(lhs, tags, evaluation): + name = lhs.get_head_name() + if len(lhs.elements) != 1: + evaluation.message_args(name, len(lhs.elements), 1) + raise AssignmentException(lhs, None) + tag = lhs.elements[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs.elements[0], 1) + raise AssignmentException(lhs, None) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, None) + if is_protected(tag, evaluation.definitions): + evaluation.message(name, "wrsym", Symbol(tag)) + raise AssignmentException(lhs, None) + return tag def get_symbol_list(list, error_callback): @@ -102,6 +107,8 @@ def get_symbol_list(list, error_callback): return values +# used in ``mathics.builtin.assignment.types`` and +# ``mathics.builtin.atomic.symbols``. def get_symbol_values(symbol, func_name, position, evaluation): name = symbol.get_name() if not name: @@ -127,32 +134,7 @@ def is_protected(tag, defin): return A_PROTECTED & defin.get_attributes(tag) -def normalize_lhs(lhs, evaluation): - """ - Process the lhs in a way that - * if it is a conditional expression, reduce it to - a shallow conditional expression - ( Conditional[Conditional[...],tst] -> Conditional[stripped_lhs, tst]) - with `stripped_lhs` the result of strip all the conditions from lhs. - * if ``stripped_lhs`` is not a ``List`` or a ``Part`` expression, evaluate the - elements. - - returns a tuple with the normalized lhs, and the lookup_name of the head in stripped_lhs. - """ - cond = None - if lhs.get_head() is SymbolCondition: - lhs, cond = unroll_conditions(lhs) - - lookup_name = lhs.get_lookup_name() - # In WMA, before the assignment, the elements of the (stripped) LHS are evaluated. - if isinstance(lhs, Expression) and lhs.get_head() not in (SymbolList, SymbolPart): - lhs = lhs.evaluate_elements(evaluation) - # If there was a conditional expression, rebuild it with the processed lhs - if cond: - lhs = Expression(cond.get_head(), lhs, cond.elements[1]) - return lhs, lookup_name - - +# Used in unroll_patterns def repl_pattern_by_symbol(expr): elements = expr.get_elements() if len(elements) == 0: @@ -180,9 +162,9 @@ def repl_pattern_by_symbol(expr): # Auxiliary routines -def rejected_because_protected(self, lhs, tag, evaluation, ignore=False): +def rejected_because_protected(self, lhs, tag, evaluation): defs = evaluation.definitions - if not ignore and is_protected(tag, defs): + if is_protected(tag, defs): if lhs.get_name() == tag: evaluation.message(self.get_name(), "wrsym", Symbol(tag)) else: @@ -191,24 +173,6 @@ def rejected_because_protected(self, lhs, tag, evaluation, ignore=False): return False -def find_tag_and_check(lhs, tags, evaluation): - name = lhs.get_head_name() - if len(lhs.elements) != 1: - evaluation.message_args(name, len(lhs.elements), 1) - raise AssignmentException(lhs, None) - tag = lhs.elements[0].get_name() - if not tag: - evaluation.message(name, "sym", lhs.elements[0], 1) - raise AssignmentException(lhs, None) - if tags is not None and tags != [tag]: - evaluation.message(name, "tag", Symbol(name), Symbol(tag)) - raise AssignmentException(lhs, None) - if is_protected(tag, evaluation.definitions): - evaluation.message(name, "wrsym", Symbol(tag)) - raise AssignmentException(lhs, None) - return tag - - def unroll_patterns(lhs, rhs, evaluation) -> Tuple[BaseElement, BaseElement]: """ Pattern[symb, pat]=rhs -> pat = (rhs/.(symb->pat)) @@ -261,581 +225,3 @@ def unroll_conditions(lhs) -> Tuple[BaseElement, Optional[Expression]]: condition = Expression(SymbolCondition, lhs, condition) # lhs._format_cache = None return lhs, condition - - -# Here starts the functions that implement `assign` for different -# kind of expressions. Maybe they should be put in a separated module, or -# maybe they should be member functions of _SetOperator. - - -def process_assign_attributes(self, lhs, rhs, evaluation, tags, upset): - """ - Process the case where lhs is of the form - `Attribute[symbol]` - """ - name = lhs.get_head_name() - if len(lhs.elements) != 1: - evaluation.message_args(name, len(lhs.elements), 1) - raise AssignmentException(lhs, rhs) - tag = lhs.elements[0].get_name() - if not tag: - evaluation.message(name, "sym", lhs.elements[0], 1) - raise AssignmentException(lhs, rhs) - if tags is not None and tags != [tag]: - evaluation.message(name, "tag", Symbol(name), Symbol(tag)) - raise AssignmentException(lhs, rhs) - attributes_list = get_symbol_list( - rhs, lambda item: evaluation.message(name, "sym", item, 1) - ) - if attributes_list is None: - raise AssignmentException(lhs, rhs) - if A_LOCKED & evaluation.definitions.get_attributes(tag): - evaluation.message(name, "locked", Symbol(tag)) - raise AssignmentException(lhs, rhs) - - def reduce_attributes_from_list(x: int, y: str) -> int: - try: - return x | attribute_string_to_number[y] - except KeyError: - evaluation.message("SetAttributes", "unknowattr", y) - return x - - attributes = reduce( - reduce_attributes_from_list, - attributes_list, - 0, - ) - - evaluation.definitions.set_attributes(tag, attributes) - - return True - - -def process_assign_context(self, lhs, rhs, evaluation, tags, upset): - lhs_name = lhs.get_head_name() - new_context = rhs.get_string_value() - if new_context is None or not valid_context_name( - new_context, allow_initial_backquote=True - ): - evaluation.message(lhs_name, "cxset", rhs) - raise AssignmentException(lhs, None) - - # With $Context in Mathematica you can do some strange - # things: e.g. with $Context set to Global`, something - # like: - # $Context = "`test`"; newsym - # is accepted and creates Global`test`newsym. - # Implement this behaviour by interpreting - # $Context = "`test`" - # as - # $Context = $Context <> "test`" - # - if new_context.startswith("`"): - new_context = evaluation.definitions.get_current_context() + new_context.lstrip( - "`" - ) - - evaluation.definitions.set_current_context(new_context) - return True - - -def process_assign_context_path(self, lhs, rhs, evaluation, tags, upset): - lhs_name = lhs.get_name() - currContext = evaluation.definitions.get_current_context() - context_path = [s.get_string_value() for s in rhs.get_elements()] - context_path = [ - s if (s is None or s[0] != "`") else currContext[:-1] + s for s in context_path - ] - if rhs.has_form("List", None) and all(valid_context_name(s) for s in context_path): - evaluation.definitions.set_context_path(context_path) - return True - else: - evaluation.message(lhs_name, "cxlist", rhs) - raise AssignmentException(lhs, None) - - -def process_assign_default(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - count = 0 - defs = evaluation.definitions - - if len(lhs.elements) not in (1, 2, 3): - evaluation.message_args(SymbolDefault, len(lhs.elements), 1, 2, 3) - raise AssignmentException(lhs, None) - focus = lhs.elements[0] - tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation - ) - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - rule = Rule(lhs, rhs) - for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation): - continue - count += 1 - defs.add_default(tag, rule) - return count > 0 - - -def process_assign_definition_values(self, lhs, rhs, evaluation, tags, upset): - name = lhs.get_head_name() - tag = find_tag_and_check(lhs, tags, evaluation) - rules = rhs.get_rules_list() - if rules is None: - evaluation.message(name, "vrule", lhs, rhs) - raise AssignmentException(lhs, None) - evaluation.definitions.set_values(tag, name, rules) - return True - - -def process_assign_format(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - count = 0 - defs = evaluation.definitions - - if len(lhs.elements) not in (1, 2): - evaluation.message_args("Format", len(lhs.elements), 1, 2) - raise AssignmentException(lhs, None) - if len(lhs.elements) == 2: - form = lhs.elements[1] - form_name = form.get_name() - if not form_name: - evaluation.message("Format", "fttp", lhs.elements[1]) - raise AssignmentException(lhs, None) - # If the form is not in defs.printforms / defs.outputforms - # add it. - for form_list in (defs.outputforms, defs.printforms): - if form not in form_list: - form_list.append(form) - else: - form_name = [ - "System`StandardForm", - "System`TraditionalForm", - "System`OutputForm", - "System`TeXForm", - "System`MathMLForm", - ] - lhs = focus = lhs.elements[0] - tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation - ) - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - rule = Rule(lhs, rhs) - for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation): - continue - count += 1 - defs.add_format(tag, rule, form_name) - return count > 0 - - -def process_assign_iteration_limit(lhs, rhs, evaluation): - """ - Set ownvalue for the $IterationLimit symbol. - """ - - rhs_int_value = rhs.get_int_value() - if ( - not rhs_int_value or rhs_int_value < 20 - ) and not rhs.get_name() == "System`Infinity": - evaluation.message("$IterationLimit", "limset", rhs) - raise AssignmentException(lhs, None) - return False - - -def process_assign_line_number_and_history_length( - self, lhs, rhs, evaluation, tags, upset -): - """ - Set ownvalue for the $Line and $HistoryLength symbols. - """ - - lhs_name = lhs.get_name() - rhs_int_value = rhs.get_int_value() - if rhs_int_value is None or rhs_int_value < 0: - evaluation.message(lhs_name, "intnn", rhs) - raise AssignmentException(lhs, None) - return False - - -def process_assign_list(self, lhs, rhs, evaluation, tags, upset): - if not ( - rhs.get_head_name() == "System`List" and len(lhs.elements) == len(rhs.elements) - ): # nopep8 - evaluation.message(self.get_name(), "shape", lhs, rhs) - return False - result = True - for left, right in zip(lhs.elements, rhs.elements): - if not self.assign(left, right, evaluation): - result = False - return result - - -def process_assign_makeboxes(self, lhs, rhs, evaluation, tags, upset): - # FIXME: the below is a big hack. - # Currently MakeBoxes boxing is implemented as a bunch of rules. - # See mathics.builtin.base contribute(). - # I think we want to change this so it works like normal SetDelayed - # That is: - # MakeBoxes[CubeRoot, StandardForm] := RadicalBox[3, StandardForm] - # rather than: - # MakeBoxes[CubeRoot, StandardForm] -> RadicalBox[3, StandardForm] - - makeboxes_rule = Rule(lhs, rhs, system=False) - definitions = evaluation.definitions - definitions.add_rule("System`MakeBoxes", makeboxes_rule, "down") - # makeboxes_defs = evaluation.definitions.builtin["System`MakeBoxes"] - # makeboxes_defs.add_rule(makeboxes_rule) - return True - - -def process_assign_minprecision(self, lhs, rhs, evaluation, tags, upset): - lhs_name = lhs.get_name() - rhs_int_value = rhs.get_int_value() - # $MinPrecision = Infinity is not allowed - if rhs_int_value is not None and rhs_int_value >= 0: - max_prec = evaluation.definitions.get_config_value("$MaxPrecision") - if max_prec is not None and max_prec < rhs_int_value: - evaluation.message("$MinPrecision", "preccon", SymbolMinPrecision) - raise AssignmentException(lhs, None) - return False - else: - evaluation.message(lhs_name, "precset", lhs, rhs) - raise AssignmentException(lhs, None) - - -def process_assign_maxprecision(self, lhs, rhs, evaluation, tags, upset): - lhs_name = lhs.get_name() - rhs_int_value = rhs.get_int_value() - if rhs.has_form("DirectedInfinity", 1) and rhs.elements[0].get_int_value() == 1: - return False - elif rhs_int_value is not None and rhs_int_value > 0: - min_prec = evaluation.definitions.get_config_value("$MinPrecision") - if min_prec is not None and rhs_int_value < min_prec: - evaluation.message("$MaxPrecision", "preccon", SymbolMaxPrecision) - raise AssignmentException(lhs, None) - return False - else: - evaluation.message(lhs_name, "precset", lhs, rhs) - raise AssignmentException(lhs, None) - - -def process_assign_messagename(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - count = 0 - defs = evaluation.definitions - if len(lhs.elements) != 2: - evaluation.message_args("MessageName", len(lhs.elements), 2) - raise AssignmentException(lhs, None) - focus = lhs.elements[0] - tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation - ) - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - rule = Rule(lhs, rhs) - for tag in tags: - # Messages can be assigned even if the symbol is protected... - # if rejected_because_protected(self, lhs, tag, evaluation): - # continue - count += 1 - defs.add_message(tag, rule) - return count > 0 - - -def process_assign_module_number(lhs, rhs, evaluation): - """ - Set ownvalue for the $ModuleNumber symbol. - """ - rhs_int_value = rhs.get_int_value() - if not rhs_int_value or rhs_int_value <= 0: - evaluation.message("$ModuleNumber", "set", rhs) - raise AssignmentException(lhs, None) - return False - - -def process_assign_options(self, lhs, rhs, evaluation, tags, upset): - lhs_elements = lhs.elements - name = lhs.get_head_name() - if len(lhs_elements) != 1: - evaluation.message_args(name, len(lhs_elements), 1) - raise AssignmentException(lhs, rhs) - tag = lhs_elements[0].get_name() - if not tag: - evaluation.message(name, "sym", lhs_elements[0], 1) - raise AssignmentException(lhs, rhs) - if tags is not None and tags != [tag]: - evaluation.message(name, "tag", Symbol(name), Symbol(tag)) - raise AssignmentException(lhs, rhs) - if is_protected(tag, evaluation.definitions): - evaluation.message(name, "wrsym", Symbol(tag)) - raise AssignmentException(lhs, None) - option_values = rhs.get_option_values(evaluation) - if option_values is None: - evaluation.message(name, "options", rhs) - raise AssignmentException(lhs, None) - evaluation.definitions.set_options(tag, option_values) - return True - - -def process_assign_numericq(self, lhs, rhs, evaluation, tags, upset): - # lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - if rhs not in (SymbolTrue, SymbolFalse): - evaluation.message("NumericQ", "set", lhs, rhs) - # raise AssignmentException(lhs, rhs) - return True - elements = lhs.elements - if len(elements) > 1: - evaluation.message("NumericQ", "argx", Integer(len(elements))) - # raise AssignmentException(lhs, rhs) - return True - target = elements[0] - if isinstance(target, Symbol): - name = target.get_name() - definition = evaluation.definitions.get_definition(name) - definition.is_numeric = rhs is SymbolTrue - return True - else: - evaluation.message("NumericQ", "set", lhs, rhs) - # raise AssignmentException(lhs, rhs) - return True - - -def process_assign_n(self, lhs, rhs, evaluation, tags, upset): - lhs, condition = unroll_conditions(lhs) - lhs, rhs = unroll_patterns(lhs, rhs, evaluation) - defs = evaluation.definitions - # If we try to set `N=4`, (issue #210) just deal with it as with a generic expression: - if lhs is SymbolN: - return assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset) - - if len(lhs.elements) not in (1, 2): - evaluation.message_args("N", len(lhs.elements), 1, 2) - raise AssignmentException(lhs, None) - if len(lhs.elements) == 1: - nprec = SymbolMachinePrecision - else: - nprec = lhs.elements[1] - focus = lhs.elements[0] - lhs = Expression(SymbolN, focus, nprec) - tags = process_tags_and_upset_dont_allow_custom( - tags, upset, self, lhs, focus, evaluation - ) - count = 0 - lhs, rhs = process_rhs_conditions(lhs, rhs, condition, evaluation) - rule = Rule(lhs, rhs) - for tag in tags: - if rejected_because_protected(self, lhs, tag, evaluation): - continue - count += 1 - defs.add_nvalue(tag, rule) - return count > 0 - - -def process_assign_other( - self, lhs, rhs, evaluation, tags=None, upset=False -) -> Tuple[bool, list]: - """ - Process special cases, performing certain side effects, like modifying - the value of internal variables that are not stored as rules. - - The function returns a tuple of a bool value and a list of tags. - If lhs is one of the special cases, then the bool variable is - True, meaning that the `Protected` attribute should not be taken into accout. - Otherwise, the value is False. - """ - tags, focus = process_tags_and_upset_allow_custom( - tags, upset, self, lhs, evaluation - ) - lhs_name = lhs.get_name() - if lhs_name == "System`$RecursionLimit": - process_assign_recursion_limit(lhs, rhs, evaluation) - elif lhs_name in ("System`$Line", "System`$HistoryLength"): - process_assign_line_number_and_history_length( - self, lhs, rhs, evaluation, tags, upset - ) - elif lhs_name == "System`$IterationLimit": - process_assign_iteration_limit(lhs, rhs, evaluation) - elif lhs_name == "System`$ModuleNumber": - process_assign_module_number(lhs, rhs, evaluation) - elif lhs_name == "System`$MinPrecision": - process_assign_minprecision(self, lhs, rhs, evaluation, tags, upset) - elif lhs_name == "System`$MaxPrecision": - process_assign_maxprecision(self, lhs, rhs, evaluation, tags, upset) - else: - return False, tags - return True, tags - - -def process_assign_part(self, lhs, rhs, evaluation, tags, upset): - """ - Special case `A[[i,j,...]]=....` - """ - defs = evaluation.definitions - if len(lhs.elements) < 1: - evaluation.message(self.get_name(), "setp", lhs) - return False - symbol = lhs.elements[0] - name = symbol.get_name() - if not name: - evaluation.message(self.get_name(), "setps", symbol) - return False - if is_protected(name, defs): - evaluation.message(self.get_name(), "wrsym", symbol) - return False - rule = defs.get_ownvalue(name) - if rule is None: - evaluation.message(self.get_name(), "noval", symbol) - return False - indices = lhs.elements[1:] - return walk_parts([rule.replace], indices, evaluation, rhs) - - -def process_assign_random_state(self, lhs, rhs, evaluation, tags, upset): - # TODO: allow setting of legal random states! - # (but consider pickle's insecurity!) - evaluation.message("$RandomState", "rndst", rhs) - raise AssignmentException(lhs, None) - - -def process_assign_recursion_limit(lhs, rhs, evaluation): - """ - Set ownvalue for the $RecursionLimit symbol. - """ - rhs_int_value = rhs.get_int_value() - # if (not rhs_int_value or rhs_int_value < 20) and not - # rhs.get_name() == 'System`Infinity': - if ( - not rhs_int_value or rhs_int_value < 20 or rhs_int_value > MAX_RECURSION_DEPTH - ): # nopep8 - - evaluation.message("$RecursionLimit", "limset", rhs) - raise AssignmentException(lhs, None) - try: - set_python_recursion_limit(rhs_int_value) - except OverflowError: - # TODO: Message - raise AssignmentException(lhs, None) - return False - - -def process_rhs_conditions(lhs, rhs, condition, evaluation): - """ - lhs = Condition[rhs, test] -> Condition[lhs, test] = rhs - """ - # To Handle `OptionValue` in `Condition` - rulopc = build_rulopc(lhs.get_head()) - rhs_name = rhs.get_head_name() - while rhs_name == "System`Condition": - if len(rhs.elements) != 2: - evaluation.message_args("Condition", len(rhs.elements), 2) - raise AssignmentException(lhs, None) - lhs = Expression( - SymbolCondition, - lhs, - rhs.elements[1].do_apply_rules([rulopc], evaluation)[0], - ) - rhs = rhs.elements[0] - rhs_name = rhs.get_head_name() - - # Now, let's add the conditions on the LHS - if condition: - lhs = Expression( - SymbolCondition, - lhs, - condition.elements[1].do_apply_rules([rulopc], evaluation)[0], - ) - return lhs, rhs - - -def process_tags_and_upset_dont_allow_custom(tags, upset, self, lhs, focus, evaluation): - focus = focus.evaluate_elements(evaluation) - name = lhs.get_head_name() - if tags is None and not upset: - name = focus.get_lookup_name() - if not name: - evaluation.message(self.get_name(), "setraw", focus) - raise AssignmentException(lhs, None) - tags = [name] - elif upset: - tags = [focus.get_lookup_name()] - else: - allowed_names = [focus.get_lookup_name()] - for name in tags: - if name not in allowed_names: - evaluation.message(self.get_name(), "tagnfd", Symbol(name)) - raise AssignmentException(lhs, None) - return tags - - -def process_tags_and_upset_allow_custom(tags, upset, self, lhs, evaluation): - name = lhs.get_head_name() - focus = lhs - focus = focus.evaluate_elements(evaluation) - if tags is None and not upset: - name = focus.get_lookup_name() - if not name: - evaluation.message(self.get_name(), "setraw", focus) - raise AssignmentException(lhs, None) - tags = [name] - elif upset: - tags = [] - if isinstance(focus, Atom): - evaluation.message(self.get_name(), "normal") - raise AssignmentException(lhs, None) - for element in focus.elements: - name = element.get_lookup_name() - tags.append(name) - else: - allowed_names = [focus.get_lookup_name()] - for element in focus.get_elements(): - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`HoldPattern", - ): - element = element.elements[0] - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`Pattern", - ): - element = element.elements[1] - if not isinstance(element, Symbol) and element.get_head_name() in ( - "System`Blank", - "System`BlankSequence", - "System`BlankNullSequence", - ): - if len(element.elements) == 1: - element = element.elements[0] - - allowed_names.append(element.get_lookup_name()) - for name in tags: - if name not in allowed_names: - evaluation.message(self.get_name(), "tagnfd", Symbol(name)) - raise AssignmentException(lhs, None) - - return tags, focus - - -# Below is a mapping from a string Symbol name into an assignment function -ASSIGNMENT_FUNCTION_MAP = { - "System`$Context": process_assign_context, - "System`$ContextPath": process_assign_context_path, - "System`$RandomState": process_assign_random_state, - "System`Attributes": process_assign_attributes, - "System`Default": process_assign_default, - "System`DefaultValues": process_assign_definition_values, - "System`DownValues": process_assign_definition_values, - "System`Format": process_assign_format, - "System`List": process_assign_list, - "System`MakeBoxes": process_assign_makeboxes, - "System`MessageName": process_assign_messagename, - "System`Messages": process_assign_definition_values, - "System`N": process_assign_n, - "System`NValues": process_assign_definition_values, - "System`NumericQ": process_assign_numericq, - "System`Options": process_assign_options, - "System`OwnValues": process_assign_definition_values, - "System`Part": process_assign_part, - "System`SubValues": process_assign_definition_values, - "System`UpValues": process_assign_definition_values, -} diff --git a/mathics/core/definitions.py b/mathics/core/definitions.py index def6b1a93..fe64e9d80 100644 --- a/mathics/core/definitions.py +++ b/mathics/core/definitions.py @@ -11,11 +11,13 @@ from typing import List, Optional -from mathics.core.atoms import String +from mathics.core.atoms import Integer, String from mathics.core.attributes import A_NO_ATTRIBUTES from mathics.core.convert.expression import to_mathics_list from mathics.core.element import fully_qualified_symbol_name from mathics.core.expression import Expression +from mathics.core.pattern import Pattern +from mathics.core.rules import Rule from mathics.core.symbols import ( Atom, Symbol, @@ -721,9 +723,6 @@ def get_ownvalue(self, name): return None def set_ownvalue(self, name, value) -> None: - from .expression import Symbol - from .rules import Rule - name = self.lookup_name(name) self.add_rule(name, Rule(Symbol(name), value)) self.clear_cache(name) @@ -759,8 +758,6 @@ def get_config_value(self, name, default=None): return default def set_config_value(self, name, new_value) -> None: - from mathics.core.expression import Integer - self.set_ownvalue(name, Integer(new_value)) def set_line_no(self, line_no) -> None: @@ -780,6 +777,25 @@ def get_history_length(self): def get_tag_position(pattern, name) -> Optional[str]: + # Strip first the pattern from HoldPattern, Pattern + # and Condition wrappings + while True: + # TODO: Not Atom/Expression, + # pattern -> pattern.to_expression() + if isinstance(pattern, Pattern): + pattern = pattern.expr + continue + if pattern.has_form("System`HoldPattern", 1): + pattern = pattern.elements[0] + continue + if pattern.has_form("System`Pattern", 2): + pattern = pattern.elements[1] + continue + if pattern.has_form("System`Condition", 2): + pattern = pattern.elements[0] + continue + break + if pattern.get_name() == name: return "own" elif isinstance(pattern, Atom): @@ -788,10 +804,8 @@ def get_tag_position(pattern, name) -> Optional[str]: head_name = pattern.get_head_name() if head_name == name: return "down" - elif head_name == "System`N" and len(pattern.elements) == 2: + elif pattern.has_form("System`N", 2): return "n" - elif head_name == "System`Condition" and len(pattern.elements) > 0: - return get_tag_position(pattern.elements[0], name) elif pattern.get_lookup_name() == name: return "sub" else: @@ -801,11 +815,18 @@ def get_tag_position(pattern, name) -> Optional[str]: return None -def insert_rule(values, rule) -> None: +def insert_rule(values: list, rule: Rule) -> None: + rhs_conds = getattr(rule, "rhs_conditions", []) for index, existing in enumerate(values): if existing.pattern.sameQ(rule.pattern): - del values[index] - break + # Check for coincidences in the replace conditions, + # it they are there. + # This ensures that the rules are equivalent even taking + # into accound the RHS conditions. + existing_rhs_conds = getattr(existing, "rhs_conditions", []) + if existing_rhs_conds == rhs_conds: + del values[index] + break # use insort_left to guarantee that if equal rules exist, newer rules will # get higher precedence by being inserted before them. see DownValues[]. bisect.insort_left(values, rule) diff --git a/mathics/core/element.py b/mathics/core/element.py index 26f525311..f061081b6 100644 --- a/mathics/core/element.py +++ b/mathics/core/element.py @@ -147,7 +147,7 @@ class KeyComparable: # FIXME: return type should be a specific kind of Tuple, not a list. # FIXME: Describe sensible, and easy to follow rules by which one # can create the kind of tuple for some new kind of element. - def get_sort_key(self) -> list: + def get_sort_key(self, pattern_sort=False) -> list: """ This returns a tuple in a way that it can be used to compare in expressions. diff --git a/mathics/core/eval/set.py b/mathics/core/eval/set.py new file mode 100644 index 000000000..97f20b17b --- /dev/null +++ b/mathics/core/eval/set.py @@ -0,0 +1,686 @@ +# -*- coding: utf-8 -*- +""" +Support for Set and SetDelayed, and other assignment-like builtins +""" + +from mathics.algorithm.parts import walk_parts + +from mathics.core.assignment import ( + AssignmentException, + find_focus, + find_tag_and_check, + get_symbol_list, + is_protected, + rejected_because_protected, + unroll_conditions, + unroll_patterns, +) +from mathics.core.atoms import Atom, Integer +from mathics.core.attributes import attribute_string_to_number, A_LOCKED +from mathics.core.evaluation import MAX_RECURSION_DEPTH, set_python_recursion_limit +from mathics.core.expression import Expression, SymbolDefault +from mathics.core.rules import Rule +from mathics.core.symbols import ( + Symbol, + SymbolFalse, + SymbolList, + SymbolMinPrecision, + SymbolMaxPrecision, + SymbolN, + SymbolTrue, + valid_context_name, +) +from mathics.core.systemsymbols import ( + SymbolCondition, + SymbolMachinePrecision, + SymbolPart, + SymbolSet, + SymbolSetDelayed, + SymbolTagSet, + SymbolTagSetDelayed, + SymbolUpSet, + SymbolUpSetDelayed, +) + + +from functools import reduce + + +# In Set* operators, the default behavior is that the +# elements of the LHS are evaluated before the assignment. +# So, if we define +# +# F[x_]:=G[x] +# +# and then +# +# M[F[x_]]:=x^2 +# +# The rule that is stored is +# +# M[G[x_]]->x^2 +# +# +# This behaviour does not aplies to a reduces subset of expressions, like +# in +# +# A={1,2,3} +# Part[A,1]:=s +# +# in a way that the result of the second line is to change a part of `A` +# A->{s, 2, 3} +# +# instead of trying to assign 1:=s +# +# Something similar happens with the Set* expressions. For example, +# the expected behavior of +# +# Set[F[x_],rhs_]:=Print["Do not set to F"] +# +# is not to forbid assignments to `G`, but to F: +# +# G[x_]:=x^4 +# still set the rule G[x_]->x^2 +# +# while +# +# F[x_]:=x^4 +# just will print the warning "Do not set to F". +# +# +NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS = ( + SymbolSet, + SymbolSetDelayed, + SymbolUpSet, + SymbolUpSetDelayed, + SymbolTagSet, + SymbolTagSetDelayed, + SymbolList, + SymbolPart, +) + + +def assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset=None): + """ + This is the default assignment. Stores a rule of the form lhs->rhs + as a value associated to each symbol listed in tags. + For special cases, such like conditions or patterns in the lhs, + lhs and rhs are rewritten in a normal form, where + conditions are associated to the lhs. + """ + defs = evaluation.definitions + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation) + # TODO: check if we can invert the order, and call this just + # in the special cases + ignore_protection = eval_set_side_effects( + self, lhs, rhs, focus, evaluation, tags, upset + ) + # In WMA, this does not happens. However, if we remove this, + # some combinatorica tests fail. + # Also, should not be at the begining? + count = 0 + rule = Rule(lhs, rhs) + position = "up" if upset else None + for tag in tags: + if not ignore_protection and rejected_because_protected( + self, lhs, tag, evaluation + ): + continue + count += 1 + defs.add_rule(tag, rule, position=position) + return count > 0 + + +# Here starts the functions that implement `assign` for different +# kind of expressions. Maybe they should be put in a separated module, or +# maybe they should be member functions of _SetOperator. + + +def eval_set_attributes(self, lhs, rhs, evaluation, tags, upset): + """ + Process the case where lhs is of the form + `Attribute[symbol]` + """ + name = lhs.get_head_name() + if len(lhs.elements) != 1: + evaluation.message_args(name, len(lhs.elements), 1) + raise AssignmentException(lhs, rhs) + tag = lhs.elements[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs.elements[0], 1) + raise AssignmentException(lhs, rhs) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, rhs) + attributes_list = get_symbol_list( + rhs, lambda item: evaluation.message(name, "sym", item, 1) + ) + if attributes_list is None: + raise AssignmentException(lhs, rhs) + if A_LOCKED & evaluation.definitions.get_attributes(tag): + evaluation.message(name, "locked", Symbol(tag)) + raise AssignmentException(lhs, rhs) + + def reduce_attributes_from_list(x: int, y: str) -> int: + try: + return x | attribute_string_to_number[y] + except KeyError: + evaluation.message("SetAttributes", "unknowattr", y) + return x + + attributes = reduce( + reduce_attributes_from_list, + attributes_list, + 0, + ) + + evaluation.definitions.set_attributes(tag, attributes) + + return True + + +def eval_set_context(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_head_name() + new_context = rhs.get_string_value() + if new_context is None or not valid_context_name( + new_context, allow_initial_backquote=True + ): + evaluation.message(lhs_name, "cxset", rhs) + raise AssignmentException(lhs, None) + + # With $Context in Mathematica you can do some strange + # things: e.g. with $Context set to Global`, something + # like: + # $Context = "`test`"; newsym + # is accepted and creates Global`test`newsym. + # Implement this behaviour by interpreting + # $Context = "`test`" + # as + # $Context = $Context <> "test`" + # + if new_context.startswith("`"): + new_context = evaluation.definitions.get_current_context() + new_context.lstrip( + "`" + ) + + evaluation.definitions.set_current_context(new_context) + return True + + +def eval_set_context_path(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + currContext = evaluation.definitions.get_current_context() + context_path = [s.get_string_value() for s in rhs.get_elements()] + context_path = [ + s if (s is None or s[0] != "`") else currContext[:-1] + s for s in context_path + ] + if rhs.has_form("List", None) and all(valid_context_name(s) for s in context_path): + evaluation.definitions.set_context_path(context_path) + return True + else: + evaluation.message(lhs_name, "cxlist", rhs) + raise AssignmentException(lhs, None) + + +def eval_set_default(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + + if len(lhs.elements) not in (1, 2, 3): + evaluation.message_args(SymbolDefault, len(lhs.elements), 1, 2, 3) + raise AssignmentException(lhs, None) + focus = lhs.elements[0] + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus) + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_default(tag, rule) + return count > 0 + + +def eval_set_definition_values(self, lhs, rhs, evaluation, tags, upset): + name = lhs.get_head_name() + tag = find_tag_and_check(lhs, tags, evaluation) + rules = rhs.get_rules_list() + if rules is None: + evaluation.message(name, "vrule", lhs, rhs) + raise AssignmentException(lhs, None) + evaluation.definitions.set_values(tag, name, rules) + return True + + +def eval_set_format(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + + if len(lhs.elements) not in (1, 2): + evaluation.message_args("Format", len(lhs.elements), 1, 2) + raise AssignmentException(lhs, None) + if len(lhs.elements) == 2: + form = lhs.elements[1] + form_name = form.get_name() + if not form_name: + evaluation.message("Format", "fttp", lhs.elements[1]) + raise AssignmentException(lhs, None) + # If the form is not in defs.printforms / defs.outputforms + # add it. + for form_list in (defs.outputforms, defs.printforms): + if form not in form_list: + form_list.append(form) + else: + form_name = [ + "System`StandardForm", + "System`TraditionalForm", + "System`OutputForm", + "System`TeXForm", + "System`MathMLForm", + ] + lhs = focus = lhs.elements[0] + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus) + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_format(tag, rule, form_name) + return count > 0 + + +def eval_set_iteration_limit(lhs, rhs, evaluation): + """ + Set ownvalue for the $IterationLimit symbol. + """ + + rhs_int_value = rhs.get_int_value() + if ( + not rhs_int_value or rhs_int_value < 20 + ) and not rhs.get_name() == "System`Infinity": + evaluation.message("$IterationLimit", "limset", rhs) + raise AssignmentException(lhs, None) + return False + + +def eval_set_line_number_and_history_length(self, lhs, rhs, evaluation, tags, upset): + """ + Set ownvalue for the $Line and $HistoryLength symbols. + """ + + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + if rhs_int_value is None or rhs_int_value < 0: + evaluation.message(lhs_name, "intnn", rhs) + raise AssignmentException(lhs, None) + return False + + +def eval_set_list(self, lhs, rhs, evaluation, tags, upset): + if not ( + rhs.get_head_name() == "System`List" and len(lhs.elements) == len(rhs.elements) + ): # nopep8 + evaluation.message(self.get_name(), "shape", lhs, rhs) + return False + result = True + for left, right in zip(lhs.elements, rhs.elements): + if not self.assign(left, right, evaluation): + result = False + return result + + +def eval_set_makeboxes(self, lhs, rhs, evaluation, tags, upset): + # FIXME: the below is a big hack. + # Currently MakeBoxes boxing is implemented as a bunch of rules. + # See mathics.builtin.base contribute(). + # I think we want to change this so it works like normal SetDelayed + # That is: + # MakeBoxes[CubeRoot, StandardForm] := RadicalBox[3, StandardForm] + # rather than: + # MakeBoxes[CubeRoot, StandardForm] -> RadicalBox[3, StandardForm] + + makeboxes_rule = Rule(lhs, rhs, system=False) + definitions = evaluation.definitions + definitions.add_rule("System`MakeBoxes", makeboxes_rule, "down") + # makeboxes_defs = evaluation.definitions.builtin["System`MakeBoxes"] + # makeboxes_defs.add_rule(makeboxes_rule) + return True + + +def eval_set_maxprecision(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + if rhs.has_form("DirectedInfinity", 1) and rhs.elements[0].get_int_value() == 1: + return False + elif rhs_int_value is not None and rhs_int_value > 0: + min_prec = evaluation.definitions.get_config_value("$MinPrecision") + if min_prec is not None and rhs_int_value < min_prec: + evaluation.message("$MaxPrecision", "preccon", SymbolMaxPrecision) + raise AssignmentException(lhs, None) + return False + else: + evaluation.message(lhs_name, "precset", lhs, rhs) + raise AssignmentException(lhs, None) + + +def eval_set_minprecision(self, lhs, rhs, evaluation, tags, upset): + lhs_name = lhs.get_name() + rhs_int_value = rhs.get_int_value() + # $MinPrecision = Infinity is not allowed + if rhs_int_value is not None and rhs_int_value >= 0: + max_prec = evaluation.definitions.get_config_value("$MaxPrecision") + if max_prec is not None and max_prec < rhs_int_value: + evaluation.message("$MinPrecision", "preccon", SymbolMinPrecision) + raise AssignmentException(lhs, None) + return False + else: + evaluation.message(lhs_name, "precset", lhs, rhs) + raise AssignmentException(lhs, None) + + +def eval_set_messagename(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + count = 0 + defs = evaluation.definitions + if len(lhs.elements) != 2: + evaluation.message_args("MessageName", len(lhs.elements), 2) + raise AssignmentException(lhs, None) + focus = lhs.elements[0] + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus) + rule = Rule(lhs, rhs) + for tag in tags: + # Messages can be assigned even if the symbol is protected... + # if rejected_because_protected(self, lhs, tag, evaluation): + # continue + count += 1 + defs.add_message(tag, rule) + return count > 0 + + +def eval_set_module_number(lhs, rhs, evaluation): + """ + Set ownvalue for the $ModuleNumber symbol. + """ + rhs_int_value = rhs.get_int_value() + if not rhs_int_value or rhs_int_value <= 0: + evaluation.message("$ModuleNumber", "set", rhs) + raise AssignmentException(lhs, None) + return False + + +def eval_set_options(self, lhs, rhs, evaluation, tags, upset): + lhs_elements = lhs.elements + name = lhs.get_head_name() + if len(lhs_elements) != 1: + evaluation.message_args(name, len(lhs_elements), 1) + raise AssignmentException(lhs, rhs) + tag = lhs_elements[0].get_name() + if not tag: + evaluation.message(name, "sym", lhs_elements[0], 1) + raise AssignmentException(lhs, rhs) + if tags is not None and tags != [tag]: + evaluation.message(name, "tag", Symbol(name), Symbol(tag)) + raise AssignmentException(lhs, rhs) + if is_protected(tag, evaluation.definitions): + evaluation.message(name, "wrsym", Symbol(tag)) + raise AssignmentException(lhs, None) + option_values = rhs.get_option_values(evaluation) + if option_values is None: + evaluation.message(name, "options", rhs) + raise AssignmentException(lhs, None) + evaluation.definitions.set_options(tag, option_values) + return True + + +def eval_set_n(self, lhs, rhs, evaluation, tags, upset): + lhs, condition = unroll_conditions(lhs) + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + defs = evaluation.definitions + # If we try to set `N=4`, (issue #210) just deal with it as with a generic expression: + if lhs is SymbolN: + return assign_store_rules_by_tag(self, lhs, rhs, evaluation, tags, upset) + + if len(lhs.elements) not in (1, 2): + evaluation.message_args("N", len(lhs.elements), 1, 2) + raise AssignmentException(lhs, None) + if len(lhs.elements) == 1: + nprec = SymbolMachinePrecision + else: + nprec = lhs.elements[1] + focus = lhs.elements[0] + lhs = Expression(SymbolN, focus, nprec) + tags, focus = eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus) + count = 0 + rule = Rule(lhs, rhs) + for tag in tags: + if rejected_because_protected(self, lhs, tag, evaluation): + continue + count += 1 + defs.add_nvalue(tag, rule) + return count > 0 + + +def eval_set_numericq(self, lhs, rhs, evaluation, tags, upset): + lhs, rhs = unroll_patterns(lhs, rhs, evaluation) + if rhs not in (SymbolTrue, SymbolFalse): + evaluation.message("NumericQ", "set", lhs, rhs) + # raise AssignmentException(lhs, rhs) + return True + elements = lhs.elements + if len(elements) > 1: + evaluation.message("NumericQ", "argx", Integer(len(elements))) + # raise AssignmentException(lhs, rhs) + return True + target = elements[0] + if isinstance(target, Symbol): + name = target.get_name() + definition = evaluation.definitions.get_definition(name) + definition.is_numeric = rhs is SymbolTrue + return True + else: + evaluation.message("NumericQ", "set", lhs, rhs) + # raise AssignmentException(lhs, rhs) + return True + + +def eval_set_part(self, lhs, rhs, evaluation, tags, upset): + """ + Special case `A[[i,j,...]]=....` + """ + defs = evaluation.definitions + if len(lhs.elements) < 1: + evaluation.message(self.get_name(), "setp", lhs) + return False + symbol = lhs.elements[0] + name = symbol.get_name() + if not name: + evaluation.message(self.get_name(), "setps", symbol) + return False + if is_protected(name, defs): + evaluation.message(self.get_name(), "wrsym", symbol) + return False + rule = defs.get_ownvalue(name) + if rule is None: + evaluation.message(self.get_name(), "noval", symbol) + return False + indices = lhs.elements[1:] + return walk_parts([rule.replace], indices, evaluation, rhs) + + +def eval_set_random_state(self, lhs, rhs, evaluation, tags, upset): + # TODO: allow setting of legal random states! + # (but consider pickle's insecurity!) + evaluation.message("$RandomState", "rndst", rhs) + raise AssignmentException(lhs, None) + + +def eval_set_recursion_limit(lhs, rhs, evaluation): + """ + Set ownvalue for the $RecursionLimit symbol. + """ + rhs_int_value = rhs.get_int_value() + # if (not rhs_int_value or rhs_int_value < 20) and not + # rhs.get_name() == 'System`Infinity': + if ( + not rhs_int_value or rhs_int_value < 20 or rhs_int_value > MAX_RECURSION_DEPTH + ): # nopep8 + + evaluation.message("$RecursionLimit", "limset", rhs) + raise AssignmentException(lhs, None) + try: + set_python_recursion_limit(rhs_int_value) + except OverflowError: + # TODO: Message + raise AssignmentException(lhs, None) + return False + + +def eval_set_side_effects( + self, lhs, rhs, focus, evaluation, tags=None, upset=False +) -> bool: + """ + Process special cases, performing certain side effects, like modifying + the value of internal variables that are not stored as rules. + + The function returns a a bool value. + If lhs is one of the special cases, then the bool variable is + True, meaning that the `Protected` attribute should not be taken into account. + Otherwise, the value is False. + """ + + lhs_name = lhs.get_name() + if lhs_name == "System`$RecursionLimit": + eval_set_recursion_limit(lhs, rhs, evaluation) + elif lhs_name in ("System`$Line", "System`$HistoryLength"): + eval_set_line_number_and_history_length(self, lhs, rhs, evaluation, tags, upset) + elif lhs_name == "System`$IterationLimit": + eval_set_iteration_limit(lhs, rhs, evaluation) + elif lhs_name == "System`$ModuleNumber": + eval_set_module_number(lhs, rhs, evaluation) + elif lhs_name == "System`$MinPrecision": + eval_set_minprecision(self, lhs, rhs, evaluation, tags, upset) + elif lhs_name == "System`$MaxPrecision": + eval_set_maxprecision(self, lhs, rhs, evaluation, tags, upset) + else: + return False + return True + + +def eval_tags_and_upset(tags, upset, self, lhs, evaluation, focus=None): + if focus is None: + allow_custom = True + focus = lhs + else: + allow_custom = False + + # Ensures that focus is the actual focus of the expression. + focus = find_focus(focus) + if ( + isinstance(focus, Expression) + and focus.head not in NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS + ): + focus = focus.evaluate_elements(evaluation) + + if tags is None and not upset: + name = focus.get_lookup_name() + if name == "": + evaluation.message(self.get_name(), "setraw", focus) + raise AssignmentException(lhs, None) + tags = [] if name is None else [name] + elif upset: + if allow_custom: + tags = [] + if isinstance(focus, Atom): + evaluation.message(self.get_name(), "normal") + raise AssignmentException(lhs, None) + for element in focus.elements: + focus_element = find_focus(element) + if focus_element is None: + continue + name = focus_element.get_lookup_name() + if name != "": + tags.append(name) + else: + name = focus.get_lookup_name() + tags = [] if name == "" else [name] + else: + if allow_custom: + allowed_names = [focus.get_lookup_name()] + for element in focus.get_elements(): + focus_element = find_focus(element) + if focus_element is None: + continue + element_tag = focus_element.get_lookup_name() + if element_tag != "": + allowed_names.append(element_tag) + else: + name = focus.get_lookup_name() + allowed_names = [] if name == "" else [name] + for name in tags: + if name not in allowed_names: + evaluation.message(self.get_name(), "tagnfd", Symbol(name)) + raise AssignmentException(lhs, None) + if len(tags) == 0: + evaluation.message(self.get_name(), "nosym", focus) + raise AssignmentException(lhs, None) + return tags, focus + + +# Below is a mapping from Symbol name (as a string) into an assignment eval function. +SET_EVAL_FUNCTION_MAP = { + "System`$Context": eval_set_context, + "System`$ContextPath": eval_set_context_path, + "System`$RandomState": eval_set_random_state, + "System`Attributes": eval_set_attributes, + "System`Default": eval_set_default, + "System`DefaultValues": eval_set_definition_values, + "System`DownValues": eval_set_definition_values, + "System`Format": eval_set_format, + "System`List": eval_set_list, + "System`MakeBoxes": eval_set_makeboxes, + "System`MessageName": eval_set_messagename, + "System`Messages": eval_set_definition_values, + "System`N": eval_set_n, + "System`NValues": eval_set_definition_values, + "System`NumericQ": eval_set_numericq, + "System`Options": eval_set_options, + "System`OwnValues": eval_set_definition_values, + "System`Part": eval_set_part, + "System`SubValues": eval_set_definition_values, + "System`UpValues": eval_set_definition_values, +} + +# Auxiliar functions + + +def normalize_lhs(lhs, evaluation): + """ + Process the lhs in a way that + * if it is a conditional expression, reduce it to + a shallow conditional expression + ( Conditional[Conditional[...],tst] -> Conditional[stripped_lhs, tst]) + with `stripped_lhs` the result of strip all the conditions from lhs. + * if ``stripped_lhs`` is not a ``List`` or a ``Part`` expression, evaluate the + elements. + + returns a tuple with the normalized lhs, and the lookup_name of the head in stripped_lhs. + """ + cond = None + if lhs.get_head() is SymbolCondition: + lhs, cond = unroll_conditions(lhs) + + lookup_name = lhs.get_lookup_name() + # In WMA, before the assignment, the elements of the (stripped) LHS are evaluated. + if ( + isinstance(lhs, Expression) + and lhs.get_head() not in NOT_EVALUATE_ELEMENTS_IN_ASSIGNMENTS + ): + lhs = lhs.evaluate_elements(evaluation) + # If there was a conditional expression, rebuild it with the processed lhs + if cond: + lhs = Expression(cond.get_head(), lhs, cond.elements[1]) + return lhs, lookup_name diff --git a/mathics/core/evaluation.py b/mathics/core/evaluation.py index 3ac098309..212bb15b0 100644 --- a/mathics/core/evaluation.py +++ b/mathics/core/evaluation.py @@ -157,7 +157,7 @@ def __init__(self) -> None: self.is_print = False self.text = "" - def get_sort_key(self) -> Tuple[bool, bool, str]: + def get_sort_key(self, pattern_sort=False) -> Tuple[bool, bool, str]: return (self.is_message, self.is_print, self.text) diff --git a/mathics/core/evaluators.py b/mathics/core/evaluators.py index 8f0dc1762..301b84176 100644 --- a/mathics/core/evaluators.py +++ b/mathics/core/evaluators.py @@ -75,7 +75,6 @@ def eval_nvalues( stored in ``evaluation.definitions``. If `prec` can not be evaluated as a number, returns None, otherwise, returns an expression. """ - # The first step is to determine the precision goal try: # Here ``get_precision`` is called with ``show_messages`` @@ -129,7 +128,7 @@ def eval_nvalues( if not result.sameQ(nexpr): result = result.evaluate(evaluation) result = eval_nvalues(result, prec, evaluation) - return result + return result # If we are here, is because there are not NValues that matches # to the expression. In such a case, if we arrive to an atomic expression, diff --git a/mathics/core/pattern.py b/mathics/core/pattern.py index b0a398d58..f0c09e8f9 100644 --- a/mathics/core/pattern.py +++ b/mathics/core/pattern.py @@ -80,18 +80,25 @@ class Pattern: def create(expr: BaseElement) -> "Pattern": """ If ``expr`` is listed in ``pattern_object`` return the pattern found there. - Otherwise, if ``expr`` is an ``Atom``, create and return ``AtomPattern`` for ``expr``. - Otherwise, create and return and ``ExpressionPattern`` for ``expr``. + Otherwise, if ``expr`` is an ``Atom``, create and return ``AtomPattern`` for ``expr``; + if it is an ``Expression`` create and return and ``ExpressionPattern`` for ``expr``. + Finally, it tries to get one object of one of these classes + by calling the method ``to_expression``, and then call the function + again with this new object as input. """ - name = expr.get_head_name() pattern_object = pattern_objects.get(name) if pattern_object is not None: return pattern_object(expr) if isinstance(expr, Atom): return AtomPattern(expr) - else: + elif isinstance(expr, Expression): return ExpressionPattern(expr) + # To handle Boxes and other possible objects that + # can be converted into expressions, convert expr to + # expression and call this function + # again: + return Pattern.create(expr.to_expression()) def match( self, diff --git a/mathics/core/rules.py b/mathics/core/rules.py index 717f9d64c..c4ba7a2a2 100644 --- a/mathics/core/rules.py +++ b/mathics/core/rules.py @@ -5,7 +5,7 @@ from mathics.core.element import KeyComparable from mathics.core.expression import Expression -from mathics.core.symbols import strip_context +from mathics.core.symbols import strip_context, SymbolTrue from mathics.core.pattern import Pattern, StopGenerator from itertools import chain @@ -19,6 +19,10 @@ def function_arguments(f): return _python_function_arguments(f) +class StopMatchConditionFailed(StopGenerator): + pass + + class StopGenerator_BaseRule(StopGenerator): pass @@ -59,7 +63,11 @@ def yield_match(vars, rest): if name.startswith("_option_"): options[name[len("_option_") :]] = value del vars[name] - new_expression = self.do_replace(expression, vars, options, evaluation) + try: + new_expression = self.do_replace(expression, vars, options, evaluation) + except StopMatchConditionFailed: + return + if new_expression is None: new_expression = expression if rest[0] or rest[1]: @@ -107,7 +115,7 @@ def yield_match(vars, rest): def do_replace(self): raise NotImplementedError - def get_sort_key(self) -> tuple: + def get_sort_key(self, pattern_sort=False) -> tuple: # FIXME: check if this makes sense: return tuple((self.system, self.pattern.get_sort_key(True))) @@ -131,12 +139,131 @@ class Rule(BaseRule): ``G[1.^2, a^2]`` """ - def __init__(self, pattern, replace, system=False) -> None: + def __ge__(self, other): + if isinstance(other, Rule): + sys, key, rhs_cond = self.get_sort_key() + sys_other, key_other, rhs_cond_other = other.get_sort_key() + if sys != sys_other: + return sys > sys_other + if key != key_other: + return key > key_other + + # larger and more complex conditions come first + len_cond, len_cond_other = len(rhs_cond), len(rhs_cond_other) + if len_cond != len_cond_other: + return len_cond_other > len_cond + if len_cond == 0: + return False + for me_cond, other_cond in zip(rhs_cond, rhs_cond_other): + me_sk = me_cond.get_sort_key(True) + o_sk = other_cond.get_sort_key(True) + if me_sk > o_sk: + return False + return True + # Follow the usual rule + return self.get_sort_key(True) >= other.get_sort_key(True) + + def __gt__(self, other): + if isinstance(other, Rule): + sys, key, rhs_cond = self.get_sort_key() + sys_other, key_other, rhs_cond_other = other.get_sort_key() + if sys != sys_other: + return sys > sys_other + if key != key_other: + return key > key_other + + # larger and more complex conditions come first + len_cond, len_cond_other = len(rhs_cond), len(rhs_cond_other) + if len_cond != len_cond_other: + return len_cond_other > len_cond + if len_cond == 0: + return False + + for me_cond, other_cond in zip(rhs_cond, rhs_cond_other): + me_sk = me_cond.get_sort_key(True) + o_sk = other_cond.get_sort_key(True) + if me_sk > o_sk: + return False + return me_sk > o_sk + # Follow the usual rule + return self.get_sort_key(True) > other.get_sort_key(True) + + def __le__(self, other): + if isinstance(other, Rule): + sys, key, rhs_cond = self.get_sort_key() + sys_other, key_other, rhs_cond_other = other.get_sort_key() + if sys != sys_other: + return sys < sys_other + if key != key_other: + return key < key_other + + # larger and more complex conditions come first + len_cond, len_cond_other = len(rhs_cond), len(rhs_cond_other) + if len_cond != len_cond_other: + return len_cond_other < len_cond + if len_cond == 0: + return False + for me_cond, other_cond in zip(rhs_cond, rhs_cond_other): + me_sk = me_cond.get_sort_key(True) + o_sk = other_cond.get_sort_key(True) + if me_sk < o_sk: + return False + return True + # Follow the usual rule + return self.get_sort_key(True) <= other.get_sort_key(True) + + def __lt__(self, other): + if isinstance(other, Rule): + sys, key, rhs_cond = self.get_sort_key() + sys_other, key_other, rhs_cond_other = other.get_sort_key() + if sys != sys_other: + return sys < sys_other + if key != key_other: + return key < key_other + + # larger and more complex conditions come first + len_cond, len_cond_other = len(rhs_cond), len(rhs_cond_other) + if len_cond != len_cond_other: + return len_cond_other < len_cond + if len_cond == 0: + return False + + for me_cond, other_cond in zip(rhs_cond, rhs_cond_other): + me_sk = me_cond.get_sort_key(True) + o_sk = other_cond.get_sort_key(True) + if me_sk < o_sk: + return False + return me_sk > o_sk + # Follow the usual rule + return self.get_sort_key(True) < other.get_sort_key(True) + + def __init__(self, pattern, replace, delayed=True, system=False) -> None: super(Rule, self).__init__(pattern, system=system) self.replace = replace + self.delayed = delayed + # If delayed is True, and replace is a nested + # Condition expression, stores the conditions and the + # remaining stripped expression. + # This is going to be used to compare and sort rules, + # and also to decide if the rule matches an expression. + conds = [] + if delayed: + while replace.has_form("System`Condition", 2): + replace, cond = replace.elements + conds.append(cond) + + self.rhs_conditions = sorted(conds) + self.strip_replace = replace def do_replace(self, expression, vars, options, evaluation): - new = self.replace.replace_vars(vars) + replace = self.replace if self.rhs_conditions == [] else self.strip_replace + for cond in self.rhs_conditions: + cond = cond.replace_vars(vars) + cond = cond.evaluate(evaluation) + if cond is not SymbolTrue: + raise StopMatchConditionFailed + + new = replace.replace_vars(vars) new.options = options # if options is a non-empty dict, we need to ensure reevaluation of the whole expression, since 'new' will @@ -159,6 +286,12 @@ def do_replace(self, expression, vars, options, evaluation): def __repr__(self) -> str: return " %s>" % (self.pattern, self.replace) + def get_sort_key(self, pattern_sort=False) -> tuple: + # FIXME: check if this makes sense: + return tuple( + (self.system, self.pattern.get_sort_key(True), self.rhs_conditions) + ) + class BuiltinRule(BaseRule): """ diff --git a/mathics/core/systemsymbols.py b/mathics/core/systemsymbols.py index a49c30a5d..dd77fac98 100644 --- a/mathics/core/systemsymbols.py +++ b/mathics/core/systemsymbols.py @@ -169,6 +169,7 @@ SymbolSeries = Symbol("System`Series") SymbolSeriesData = Symbol("System`SeriesData") SymbolSet = Symbol("System`Set") +SymbolSetDelayed = Symbol("System`SetDelayed") SymbolSign = Symbol("System`Sign") SymbolSimplify = Symbol("System`Simplify") SymbolSin = Symbol("System`Sin") @@ -186,6 +187,8 @@ SymbolSubsuperscriptBox = Symbol("System`SubsuperscriptBox") SymbolSuperscriptBox = Symbol("System`SuperscriptBox") SymbolTable = Symbol("System`Table") +SymbolTagSet = Symbol("System`TagSet") +SymbolTagSetDelayed = Symbol("System`TagSetDelayed") SymbolTeXForm = Symbol("System`TeXForm") SymbolThrow = Symbol("System`Throw") SymbolToString = Symbol("System`ToString") @@ -194,5 +197,7 @@ SymbolUndefined = Symbol("System`Undefined") SymbolUnequal = Symbol("System`Unequal") SymbolUnevaluated = Symbol("System`Unevaluated") +SymbolUpSet = Symbol("System`UpSet") +SymbolUpSetDelayed = Symbol("System`UpSetDelayed") SymbolUpValues = Symbol("System`UpValues") SymbolXor = Symbol("System`Xor") diff --git a/mathics/packages/CellsToTeX/CellsToTeX.m b/mathics/packages/CellsToTeX/CellsToTeX.m new file mode 100644 index 000000000..7b13a1a6b --- /dev/null +++ b/mathics/packages/CellsToTeX/CellsToTeX.m @@ -0,0 +1,2827 @@ +(* ::Package:: *) + +(* ::Section:: *) +(*Usage messages*) + + +BeginPackage["CellsToTeX`"] + + +Unprotect["`*"] +ClearAll["`*"] + + +(* ::Subsection:: *) +(*Public*) + + +CellToTeX::usage = +"\ +CellToTeX[cell] \ +returns String with TeX code representing given cell. Returned TeX code \ +contains converted cell contents and data extracted from Cell options." + + +CellsToTeXPreamble::usage = +"\ +CellsToTeXPreamble[] \ +returns String with TeX code setting global properties of mmacells package, \ +suitable for inclusion in document preamble." + + +CellsToTeXException::usage = +"\ +CellsToTeXException \ +is a symbol to which CellsToTeX package exception messages are attached. \ + +CellsToTeXException[errType, errSubtype, ...] \ +is used as Throw tag identifying CellsToTeX package exceptions." + + +(* ::Subsection:: *) +(*Configuration*) + + +Begin["`Configuration`"] + + +Unprotect["`*"] +ClearAll["`*"] + + +(* ::Subsubsection:: *) +(*Variables*) + + +$supportedCellStyles::usage = +"\ +$supportedCellStyles \ +is a pattern matching all cell styles supported by CellToTeX function." + + +$cellStyleOptions::usage = +"\ +$cellStyleOptions \ +is a List of rules with left hand sides being Lists of two elements. First \ +element is a pattern matching particular cell styles, second element is \ +option name. Right hand sides of rules are default values of options that \ +will be used for styles matching said pattern." + + +$commandCharsToTeX::usage = +"\ +$commandCharsToTeX \ +is a List of three rules. With left hand sides being: eascape character and \ +argument delimiters used by TeX formatting commands. Right hand sides are \ +strings used to represent escaped commandChars." + + +$basicBoxes::usage = +"\ +$basicBoxes \ +is a pattern matching boxes that can be converted to correct TeX verbatim \ +code without any BoxRules." + + +$linearBoxesToTeX::usage = +"\ +$linearBoxesToTeX \ +is a List of rules transforming \"linear\" boxes to TeX Verbatim code." + + +$boxesToFormattedTeX::usage = +"\ +$boxesToFormattedTeX \ +is a List of rules transforming boxes to formatted TeX verbatim code." + + +$boxHeadsToTeXCommands::usage = +"\ +$boxHeadsToTeXCommands \ +is a List of rules assigning TeX command specifications to box heads. \ +Right hand side of rules can be a List of two elements with first being a \ +String with TeX command name and second being number of arguments of TeX \ +command." + + +$stringsToTeX::usage = +"\ +$stringsToTeX \ +is a List of rules transforming string fragments to TeX suitable for \ +inclusion in formatted TeX verbatim code." + + +$annotationTypesToTeX::usage = +"\ +$annotationTypesToTeX \ +is List of rules with left hand sides being String with annotation type and \ +right hand sides being lists of two elements. First element of pair is a \ +String with key used in TeX mmaCell optional argument, or None if there's no \ +key associated with annotation type. Second element is String with TeX \ +command used to annotate verbatim code." + + +$currentValueObj::usage = +"\ +$currentValueObj \ +is a notebook or front end object used as basis for CurrentValue evaluations \ +extracting styles needed for conversion of some boxes." + + +(* ::Subsubsection:: *) +(*Utilities*) + + +headRulesToBoxRules::usage = +"\ +headRulesToBoxRules[head -> {\"name\", argsNo}] \ +returns delayed rule that transforms box expression, with given head, to TeX \ +formatting command with given name. Box can contain argsNo arguments and \ +options.\ + +headRulesToBoxRules[{rule1, rule2, ...}] \ +returns List of transformed rules." + + +defaultAnnotationType::usage = +"\ +defaultAnnotationType[sym] or defaultAnnotationType[\"name\"] \ +returns String with default syntax annotation type of given symbol sym \ +or of symbol with given \"name\"." + + +texMathReplacement::usage = +"\ +texMathReplacement[\"str\"] \ +returns String with TeX code representing given String \"str\". By default \ +DownValues of texMathReplacement are used by CellsToTeXPreamble." + + +texMathReplacementRegister::usage = +"\ +texMathReplacementRegister[\"str\"] \ +defines TeX code math replacement for given String \"str\", if it's not \ +already defined. Returns \"str\"." + + +makeString::usage = +"\ +makeString[boxes] \ +returns String representation of given boxes. \ +This function can be used on right hand side of user provided BoxRules. \ +It will apply BoxRules to its argument. \ +Definitions for this function are locally created from user provided BoxRules \ +on each evaluation of boxesToString function." + + +makeStringDefault::usage = +"\ +makeStringDefault[boxes] \ +returns String representation of given boxes. \ +This function can be used on right hand side of user provided BoxRules. \ +It will not apply BoxRules to its argument. \ +This function is called by makeString function, when no user provided BoxRule \ +applies." + + +charToTeX::usage = +"\ +charToTeX[\"x\"] \ +returns String with TeX code, representing given character, suitable for \ +inclusion in formatted TeX verbatim code." + + +fileNameGenerator::usage = +"\ +fileNameGenerator[boxes, exportFormat] \ +returns String with file name created from Hash of given boxes with proper \ +extension for given exportFormat." + + +processorDataLookup::usage = +"\ +processorDataLookup[processorCall, data, key] \ +returns value associated with given key, if key is in given data. Otherwise \ +throws CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"].\ + +processorDataLookup[processorCall, data, {key1, key2, ...}] \ +returns List of values associated with given keys, if all of them are in \ +given data. Otherwise throws \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"]." + + +syntaxBox::usage = +"\ +syntaxBox[boxes, type] \ +represents boxes that in an expression perform sytnax role of given type." + + +(* ::Subsubsection:: *) +(*Processors*) + + +extractCellOptionsProcessor::usage = +"\ +extractCellOptionsProcessor[{\"Boxes\" -> boxes, ...}] \ +returns List of given options with following modifications. If boxes is a \ +Cell expression, then all it's options are appended to given options. \ +Otherwise given options are returned unchanged. + +If \"Boxes\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown." + + +cellLabelProcessor::usage = +"\ +cellLabelProcessor[{\ +\"TeXOptions\" -> texOptions, \"CurrentCellIndex\" -> ..., +\"PreviousIntype\" -> ..., \"Indexed\" -> ..., \"Intype\" -> ...,\ +\"CellLabel\" -> ..., \"CellIndex\" -> ..., \"CellForm\" -> ..., ...\ +}] \ +returns List of given options with following modifications. texOptions \ +can have appended \"label\", \"index\" and/or \"form\" key. \"Indexed\", \ +\"CellIndex\" and \"Intype\" options are added/updated.\ + +If \"TeXOptions\", \"CurrentCellIndex\" or \"PreviousIntype\" key is not \ +present CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is \ +thrown." + + +trackCellIndexProcessor::usage = +"\ +trackCellIndexProcessor[{\ +\"Indexed\" -> indexed, \"CellIndex\" -> cellIndex, \"Intype\" -> intype, ...\ +}] \ +returns unmodified List of given options. For indexed cells sets \ +\"CurrentCellIndex\" and \"PreviousIntype\" options, of CellToTeX function, \ +to cellIndex and intype respectively. + +If \"Indexed\", \"CellIndex\" or \"Intype\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown." + + +annotateSyntaxProcessor::usage = +"\ +annotateSyntaxProcessor[{\ +\"Boxes\" -> boxes, \"TeXOptions\" -> texOptions, \ +\"BoxRules\" -> boxRules, ...\ +}] \ +returns List of given options with following modifications. boxes have \ +annotated syntax. boxRules have appended rules transforming annotated \ +expressions. texOptions have appended options representing commonest \ +annotations of annotated syntax elements.\ + +If \"Boxes\" or \"TeXOptions\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown." + + +toInputFormProcessor::usage = +"\ +toInputFormProcessor[{\"Boxes\" -> boxes, ...}] \ +returns List of given options with boxes converted to input form boxes.\ + +If \"Boxes\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown.\ + +If boxes represent syntactically invalid expression \ +CellsToTeXException[\"Invalid\", \"Boxes\"] is thrown. \ + +If parsing of string with InputForm of expression represented by boxes fails \ +CellsToTeXException[\"Failed\", \"Parser\"] is thrown." + + +messageLinkProcessor::usage = +"\ +messageLinkProcessor[{\ +\"Boxes\" -> boxes, \"TeXOptions\" -> texOptions, ...\ +}] \ +returns List of given options with following modifications. If boxes contain \ +message link, proper messagelink option is appended to texOptions. \ +Otherwise options are returned unchanged.\ + +If \"Boxes\" or \"TeXOptions\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown." + + +boxRulesProcessor::usage = +"\ +boxRulesProcessor[{\ +\"BoxRules\" -> boxRules, \"StringRules\" -> stringRules, \ +\"NonASCIIHandler\" -> nonASCIIHandler, ...\ +}] \ +returns List of given options with following modifications. If \ +nonASCIIHandler is not Identity, \"StringRules\" have appended rule \ +converting non-ASCII characters using nonASCIIHandler. If after modification \ +\"StringRules\" are not empty, \"BoxRules\" have appended rule replaceing \ +string using \"StringRules\"." + + +boxesToTeXProcessor::usage = +"\ +boxesToTeXProcessor[{\"Boxes\" -> boxes, \"BoxRules\" -> boxRules, ...}] \ +returns List of given options with \"TeXCode\" option added. This option's \ +value is a String with TeX representation of given boxes obtained by applying \ +given boxRules.\ + +If \"Boxes\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown.\ + +If boxes contain box not covered by boxRules, then \ +CellsToTeXException[\"Unsupported\", \"Box\"] is thrown." + + +mmaCellProcessor::usage = +"\ +mmaCellProcessor[{\ +\"TeXCode\" -> texCode, \"Style\" -> style, \"TeXOptions\" -> texOptions, \ +\"Indentation\" -> indentation, ...\ +}] \ +returns List of given options with \"TeXCode\" option modified. Its lines are \ +indented using given indentation and whole code is wrapped with TeX mmaCell \ +environment with given style and texOptions.\ + +If \"TeXCode\", \"Style\" or \"TeXOptions\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown." + + +exportProcessor::usage = +"\ +exportProcessor[{\ +\"Boxes\" -> boxes, \"Style\" -> style, \ +\"AdditionalCellOptions\" -> additionalCellOptions, \ +\"ExportFormat\" -> exportFormat, \"FileNameGenerator\" -> fileNameGenerator, \ +...\ +}] \ +exports given boxes to a file and returns List of given options with \ +\"FileName\" option added. boxes, before export, are wrapped, if necessary, \ +with BoxData and Cell with given style and additionalCellOptions. boxes are \ +exported using given exportFormat. Name of file is a result of evaluation of \ +given fileNameGenerator function with boxes as first argument and \ +exportFormat as second.\ + +If \"Boxes\" or \"Style\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown.\ + +If Export command fails CellsToTeXException[\"Failed\", \"Export\"] is thrown." + + +mmaCellGraphicsProcessor::usage = +"\ +mmaCellGraphicsProcessor[{\ +\"FileName\" -> filename, \"Style\" -> style, \"TeXOptions\" -> texOptions, \ +...\ +}] \ +returns List of given options with \"TeXCode\" option added. This option's \ +value is a String with TeX mmaCellGraphics command including file with given \ +filename as graphics. TeX command has given style, texOptions and filename as \ +arguments.\ + +If \"FileName\", \"Style\" or \"TeXOptions\" key is not present \ +CellsToTeXException[\"Missing\", \"Keys\", \"ProcessorArgument\"] is thrown." + + +End[] + + +(* ::Subsection:: *) +(*Internal*) + + +Begin["`Internal`"] + + +ClearAll["`*"] + + +$texEscape::usage = +"\ +$texEscape \ +is a String used to escape a character in non-verbatim TeX code." + + +$texSpecialChars::usage = +"\ +$texSpecialChars \ +is a string pattern matching characters that need to be escaped in \ +non-verbatim TeX code." + + +throwException::usage = +"\ +throwException[thrownBy, {errType, errSubtype, ...}, {val1, val2, ...}] or \ +\ +throwException[\ +HoldComplete[thrownBy], {errType, errSubtype, ...}, {val1, val2, ...}\ +] or \ +\ +throwException[\ +thrownBy, {errType, errSubtype, ...}, HoldComplete[val1, val2, ...]\ +] \ +throws Failure object tagged with \ +CellsToTeXException[errType, errSubtype, ...]. Failure object contains List \ +with thrownBy, CellsToTeXException[errType, errSubtype, ...] and given vali, \ +wrapped with HoldForm, associated to \"MessageParameters\" key and message \ +name apropriate for given exception types associated to \"MessageTemplate\" \ +key.\ + +throwException[thrownBy, \"errType\", vals] \ +is equivalent to throwException[thrownBy, {\"errType\"}, vals].\ + +throwException[thrownBy, \"errType\"] \ +uses empty List of values.\ + +throwException[thrownBy, errTypes, vals, \"messageName\"] \ +uses CellsToTeXException::messageName as \"MessageTemplate\" key." + + +handleException::usage = +"\ +handleException[value, tag] \ +if tag is CellsToTeXException[...] expression and value is a Failure object \ +prints exception message and returns value, for CellsToTeXException[...] tag \ +and non-Failure value returns $Failed and prints generic exception message, \ +otherwise throws value with given tag." + + +catchException::usage = +"\ +catchException[body] \ +evaluates body. If CellsToTeXException was thrown in process of body \ +evaluation prints apropriate exception message and returns Failure object. \ +Otherwise returns result of body evalaution." + + +rethrowException::usage = +"\ +rethrowException[rethrownBy][body] \ +evaluates body. If CellsToTeXException was thrown in process of body \ +evaluation re-throws this exception with rethrownBy as first argument of \ +\"MessageParameters\" List. Other throws are not caught. If nothing was \ +thrown result of body evalaution is returned." + + +dataLookup::usage = +"\ +dataLookup[data, key] \ +returns value associated with given key, if key is in given data. Otherwise \ +throws CellsToTeXException[\"Missing\", \"Keys\"].\ + +dataLookup[data, {key1, key2, ...}] \ +returns List of values associated with given keys, if all of them are in \ +given data. Otherwise throws CellsToTeXException[\"Missing\", \"Keys\"]." + + +extractStyleOptions::usage = +"\ +extractStyleOptions[style, {{stylePatt1, opt1} -> val1, ...}] \ +returns List of options for given style. Returned List contains rules of form \ +opti -> vali for which in given list of rules there exist rule +{{stylePatti, opti} -> vali} such that given style matches stylePatti." + + +boxesToInputFormBoxes::usage = +"\ +boxesToInputFormBoxes[boxes] \ +returns given boxes converted to InputForm.\ + +If boxes represent syntactically invalid expression \ +CellsToTeXException[\"Invalid\", \"Boxes\"] is thrown.\ + +If parsing of string with InputForm of expression represented by given boxes \ +fails CellsToTeXException[\"Failed\", \"Parser\"] is thrown." + + +boxesToString::usage = +"\ +boxesToString[boxes, boxRules] \ +returns String representation of given boxes, obtained by applying given \ +boxRules.\ + +If value of FormatType option is different than InputForm and OutputForm \ +CellsToTeXException[\"Unsupported\", \"FormatType\"] is thrown." + + +defaultOrFirst::usage = +"\ +defaultOrFirst[list, default] \ +returns default if it's member of given list, otherwise first element of list \ +is returned. Given list should have at least one element." + + +commonestAnnotationTypes::usage = +"\ +commonestAnnotationTypes[boxes, allowedTypes, specialChars] \ +returns List of rules with left hand sides being strings and right hand sides \ +being most common annotation types of those strings in given boxes. \ +Only annotation types matching allowedTypes are taken into consideration when \ +determining commonest annotation types. If specialChars is True all strings \ +in boxes are inspected, if it's False only those composed of ASCII characters \ +are inspected." + + +annotationTypesToKeyVal::usage = +"\ +annotationTypesToKeyVal[{sym1 -> type1, ...}, {type1 -> key1, ...}] \ +returns List of rules representing key-value pairs for inclusion as optional \ +argument of mmaCell TeX environment. Returned List can contain keys \ +representing syntax coloring." + + +annotationRulesToBoxRules::usage = +"\ +annotationRulesToBoxRules[{type1 -> {key1, command1}, ...}] \ +returns List of delayed rules that transform syntaxBox with given typei to \ +TeX commandi." + + +labelToCellData::usage = +"\ +labelToCellData[\"cellLabel\"] \ +returns List containing three elements, extracted from given \"cellLabel\": \ +cell type (In, Out, or None), \ +cell index (Integer or None) and \ +cell form (String or None)." + + +labelToKeyVal::usage = +"\ +labelToKeyVal[cellLabel, cellIndex, cellForm, currentCellIndex] \ +returns List containing two elements. First element is List of rules \ +representing key-value pairs for inclusion as optional argument of mmaCell \ +TeX environment, it can contain \"label\", \"index\" and \"form\" keys. \ +Second element is new value for of currentCellIndex." + + +optionValueToTeX::usage = +"\ +optionValueToTeX[val] \ +returns given TeX option value val suitable for inclusion as value in TeX \ +key-value argument." + + +optionsToTeX::usage = +"\ +optionsToTeX[{key1 -> val1, key2 :> val2, ...}] \ +returns String with given rules transformed to TeX key-value pairs.\ + +optionsToTeX[pre, {key1 -> val1, key2 :> val2, ...}, post] \ +wraps result with pre and post, if result is not empty." + + +mergeAdjacentTeXDelims::usage = +"\ +mergeAdjacentTeXDelims[startDelim, endDelim][texCode] \ +returns String with given texCode in which code fragments, delimited by \ +startDelim and endDelim, separated only by tabs or spaces, are merged \ +together." + + +mergeAdjacentTeXCommands::usage = +"\ +mergeAdjacentTeXCommands[cmd, argStart, argEnd][texCode] \ +returns String with given texCode in which occurences of commands cmd with \ +single arguments, separated only by tabs or spaces, are merged into one \ +command." + + +templateBoxDisplayBoxes::usage = +"\ +templateBoxDisplayBoxes[templateBox] \ +returns boxes representing display form of given templateBox." + + +extractMessageLink::usage = +"\ +extractMessageLink[boxes] \ +returns String with URI ending of message link, if such link is in boxes. \ +Otherwise returns Missing[\"NotFound\"]." + + +prettifyPatterns::usage = +"\ +prettifyPatterns[expr] \ +returns expr with changed Pattern names. Names not used in pattern are \ +removed. Names used in pattern are replaced by strings." + + +formatToExtension::usage = +"\ +formatToExtension[\"format\"] \ +returns String with file extension for given export format. Extension \ +contains leading dot. For unknown formats empty string is returned." + + +$convertRecursiveOption::usage = +"\ +$convertRecursiveOption \ +is name of System`Convert`CommonDump`RemoveLinearSyntax function option \ +apropriate for used Mathematica version." + + +End[] + + +(* ::Subsection:: *) +(*Package*) + + +Begin["`Package`"] + + +ClearAll["`*"] + + +addIncorrectArgsDefinition::usage = +"\ +addIncorrectArgsDefinition[sym] \ +adds catch-all definition to given symbol sym, that, when matched, throws \ +CellsToTeXException[\"Error\", \"IncorrectArguments\"].\ + +addIncorrectArgsDefinition[\"symName\"] \ +adds definition to symbol with given name. If given string is not a symbol \ +name, throws CellsToTeXException[\"Error\", \"NotSymbolName\"]. If symbol \ +with given name has defined own value, throws \ +CellsToTeXException[\"Error\", \"UnwantedEvaluation\"]" + + +End[] + + +(* ::Subsection:: *) +(*Backports*) + + +Begin["`Backports`"] + + +Unprotect["`*"] +ClearAll["`*"] + + +If[$VersionNumber < 10, + FirstCase::usage = "\ +FirstCase[{e1, e2, ...}, pattern] \ +gives the first ei to match pattern, or Missing[\"NotFound\"] if none \ +matching pattern is found.\ + +FirstCase[{e1, e2, ...}, pattern -> rhs] \ +gives the value of rhs corresponding to the first ei to match pattern.\ + +FirstCase[expr, pattern, default] \ +gives default if no element matching pattern is found.\ + +FirstCase[expr, pattern, default, levelspec] \ +finds only objects that appear on levels specified by levelspec.\ + +FirstCase[pattern] \ +represents an operator form of FirstCase that can be applied to an expression.\ + +This is a backport of FirstCase from Mathematica versions >= 10."; + + + Association::usage = "\ +Association[key1 -> val1, key2 :> val2, ...] \ +is a very limited backport of Association from Mathematica version 10.\ + +Association[key1 -> val1, key2 :> val2, ...][key] \ +if key exists in association returns value associated with given key, \ +otherwise returns Missing[\"KeyAbsent\", key]."; + + + AssociationQ::usage = "\ +AssociationQ[expr] \ +gives True if expr is a valid Association object, and False otherwise. \ +This is a backport from Mathematica version 10."; + + + Key::usage = "\ +Key[key] \ +represents a key used to access a value in an association. This is a backport \ +from Mathematica version 10."; + + + Lookup::usage = "\ +Lookup[assoc, key] \ +looks up the value associated with key in the association assoc, or \ +Missing[\"KeyAbsent\"].\ + +Lookup[assoc, key, default] \ +gives default if key is not present.\ + +This is a very limited backport of Lookup from Mathematica version 10."; + + + Failure::usage = "\ +Failure[tag, assoc] \ +represents a failure of a type indicated by tag, with details given by the \ +association assoc. This is a backport from Mathematica version 10." +] + + +End[] + + +(* ::Section:: *) +(*Exception messages*) + + +CellsToTeXException::unsupported = "\ +`3`: `4` is not one of supported: `5`. \ +Exception occurred in `1`." + +CellsToTeXException::failed = "\ +Evaluation of following `3` expression failed: `4`. \ +Exception occurred in `1`." + +CellsToTeXException::invalid = "\ +Following elements of type `3` are invalid: `4`. \ +Exception occurred in `1`." + +CellsToTeXException::missing = "\ +Following elements of type `3` are missing: `4`. \ +Available elements of type `3` are: `5`. \ +Exception occurred in `1`." + +CellsToTeXException::missingProcArg = "\ +Processor didn't receive required data with following keys: `4`. \ +Passed data keys are: `5`. \ +Exception occurred in `1`." + +CellsToTeXException::missingProcRes = "\ +Required keys: `4` are not present in data returned by processor: `6`. \ +Data contained only `5` keys. \ +Exception occurred in `1`." + +CellsToTeXException::missingCellStyle = "\ +Couldn't extract cell style from given boxes. \ +Either use \"Style\" option or provide Cell expression with defined style. \ +Given boxes are: `3`. \ +Exception occurred in `1`." + +CellsToTeXException::error = "\ +An internal error occurred. \ +`1` expression caused `2`. \ +Please inform the package maintainer about this problem." + +CellsToTeXException::unknownError = "\ +An internal error occurred. \ +`1` was thrown tagged with `2`. \ +Please inform the package maintainer about this problem." + + +(* ::Section:: *) +(*Implementation*) + + +(* ::Subsection:: *) +(*Dependencies*) + + +Needs["`SyntaxAnnotations`"] + + +Begin["`Private`"] + + +ClearAll["`*"] + + +$ContextPath = + Join[ + { + "CellsToTeX`Configuration`", + "CellsToTeX`Package`", + "CellsToTeX`Internal`", + "CellsToTeX`Backports`" + }, + $ContextPath + ] + + +(* Dummy evaluation to laod System`Convert`TeXFormDump` context. *) +Convert`TeX`BoxesToTeX + + +(* ::Subsection:: *) +(*Package*) + + +(* ::Subsubsection:: *) +(*addIncorrectArgsDefinition*) + + +addIncorrectArgsDefinition[sym_Symbol] := ( + functionCall:sym[___] := + throwException[functionCall, {"Error", "IncorrectArguments"}] +) + +functionCall:addIncorrectArgsDefinition[str_String] := ( + Check[ + Symbol[str], + throwException[functionCall, {"Error", "NotSymbolName"}, {str}], + Symbol::symname + ]; + With[{heldSym = ToExpression[str, InputForm, HoldComplete]}, + If[ValueQ @@ heldSym, + throwException[functionCall, {"Error", "UnwantedEvaluation"}, + heldSym + ] + ]; + + addIncorrectArgsDefinition @@ heldSym + ] +) + + +(* ::Subsubsection:: *) +(*Common operations*) + + +addIncorrectArgsDefinition /@ + Names["CellsToTeX`Package`" ~~ Except["$"] ~~ Except["`"]...] + + +(* ::Subsection:: *) +(*Backports*) + + +(* ::Subsubsection:: *) +(*FirstCase*) + + +If[$VersionNumber < 10, + SetAttributes[FirstCase, HoldRest]; + + Options[FirstCase] = Options[Cases]; + + FirstCase[ + expr_, pattOrRule_, Shortest[default_:Missing["NotFound"], 1], + Shortest[levelspec_:{1}, 2], opts:OptionsPattern[] + ] := + Replace[Cases[expr, pattOrRule, levelspec, 1, opts], + {{} :> default, {match_} :> match} + ]; + + FirstCase[pattOrRule_][expr_] := FirstCase[expr, pattOrRule] +] + + +(* ::Subsubsection:: *) +(*Association*) + + +If[$VersionNumber < 10, + (assoc_Association)[key_] := Lookup[assoc, key]; + + + Association /: Extract[ + assoc_Association, fullKey:(Key[key_] | key_String), head_:Identity + ] := + FirstCase[assoc, + _[Verbatim[key], val_] :> head@val, + head@Missing["KeyAbsent", fullKey] + ]; + + + Association /: Append[ + Association[rules___], + {newRules:(_Rule | _RuleDelayed)...} | newRule:(_Rule | _RuleDelayed) + ] := + With[{newRulesList = {newRules, newRule}}, + Association @@ Join[ + FilterRules[{rules}, Except[newRulesList]], + newRulesList + ] + ] +] + + +(* ::Subsubsection:: *) +(*AssociationQ*) + + +If[$VersionNumber < 10, + AssociationQ[Association[(_Rule | _RuleDelayed)...]] = True; + + AssociationQ[_] = False +] + + +(* ::Subsubsection:: *) +(*Lookup*) + + +If[$VersionNumber < 10, + SetAttributes[Lookup, HoldAllComplete]; + + Lookup[assoc_?AssociationQ, key_, default_] := + FirstCase[assoc, _[Verbatim[key], val_] :> val, default]; + + Lookup[assoc_?AssociationQ, key_] := + Lookup[assoc, key, Missing["KeyAbsent", key]] +] + + +(* ::Subsubsection:: *) +(*Failure*) + + +If[$VersionNumber < 10, + Failure /: MakeBoxes[Failure[tag_, assoc_Association], StandardForm] := + With[ + { + msg = assoc["MessageTemplate"], + msgParam = assoc["MessageParameters"], + type = assoc["Type"] + } + , + ToBoxes @ Interpretation[ + "Failure" @ Panel @ Grid[ + { + { + Style["\[WarningSign]", "Message", FontSize -> 35] + , + Style["Message:", FontColor->GrayLevel[0.5]] + , + ToString[ + StringForm[msg, Sequence @@ msgParam], + StandardForm + ] + }, + { + SpanFromAbove, + Style["Tag:", FontColor->GrayLevel[0.5]], + ToString[tag, StandardForm] + }, + { + SpanFromAbove, + Style["Type:", FontColor->GrayLevel[0.5]], + ToString[type, StandardForm] + } + }, + Alignment -> {Left, Top} + ], + Failure[tag, assoc] + ] /; msg =!= Missing["KeyAbsent", "MessageTemplate"] && + msgParam =!= Missing["KeyAbsent", "MessageParameters"] && + msgParam =!= Missing["KeyAbsent", "Type"] + ] +] + + +(* ::Subsubsection:: *) +(*Common operations*) + + +Protect @ Evaluate @ Names[ + "CellsToTeX`Backports`" ~~ Except["$"] ~~ Except["`"]... +] + + +(* ::Subsection:: *) +(*Internal*) + + +(* ::Subsubsection:: *) +(*Common operations*) + + +addIncorrectArgsDefinition /@ + Names["CellsToTeX`Internal`" ~~ Except["$"] ~~ Except["`"]...] + + +(* ::Subsubsection:: *) +(*$texEscape*) + + +$texEscape = "\\" + + +(* ::Subsubsection:: *) +(*$texSpecialChars*) + + +$texSpecialChars = "#" | "%" | "{" | "}" | "\\" + + +(* ::Subsubsection:: *) +(*throwException*) + + +SetAttributes[throwException, HoldFirst] + + +throwException[ + HoldComplete[thrownBy_] | thrownBy_, + {"Unsupported", elementType_, subTypes___}, + (List | HoldComplete)[unsupported_, supported_], + messageName:(_String | Automatic):Automatic +] := + With[{supportedPretty = prettifyPatterns /@ supported}, + throwException[ + thrownBy, + {"Unsupported", elementType, subTypes}, + HoldComplete[elementType, unsupported, supportedPretty], + messageName + ] + ] + +throwException[ + HoldComplete[thrownBy_] | thrownBy_, + {"Missing", elementType_, subTypes___}, + (List | HoldComplete)[missing_, available_], + messageName:(_String | Automatic):Automatic +] := + throwException[ + thrownBy, + {"Missing", elementType, subTypes}, + HoldComplete[elementType, missing, available], + messageName + ] + +throwException[ + HoldComplete[thrownBy_] | thrownBy_, + {"Invalid", elementType_, subTypes___}, + (List | HoldComplete)[boxes_], + messageName:(_String | Automatic):Automatic +] := + throwException[ + thrownBy, + {"Invalid", elementType, subTypes}, + HoldComplete[elementType, boxes], + messageName + ] + +throwException[ + HoldComplete[thrownBy_] | thrownBy_, + {types__} | type_String, + (List | HoldComplete)[vals___] | PatternSequence[], + messageNameArg:(_String | Automatic):Automatic +] := + With[ + { + tag = CellsToTeXException[types, type], + messageName = + If[messageNameArg === Automatic, + Replace[First[{types, type}], { + mainType_String :> + With[{msgName = ToLowerCase[mainType]}, + msgName /; ValueQ @ + MessageName[CellsToTeXException, msgName] + ], + _ -> "error" + }] + (* else *), + messageNameArg + ] + } + , + Throw[ + Failure[CellsToTeXException, + Association[ + "MessageTemplate" :> + MessageName[CellsToTeXException, messageName], + "MessageParameters" -> + List @@ HoldForm /@ HoldComplete[thrownBy, tag, vals], + "Type" -> {types, type} + ] + ], + tag + ] + ] + + +(* ::Subsubsection:: *) +(*handleException*) + + +handleException[ + failure:Failure[_, assoc_Association], + tag_CellsToTeXException +] := + With[ + { + heldMsgName = + Quiet[Extract[assoc, "MessageTemplate", Hold], Extract::keyw] + } + , + ( + Message @@ Join[ + heldMsgName, + Hold @@ Lookup[ + assoc, "MessageParameters", + {HoldForm["Unknown"], HoldForm[tag]} + ] + ]; + failure + ) /; heldMsgName =!= Hold@Missing["KeyAbsent", "MessageTemplate"] + ] + +handleException[val_, tag_CellsToTeXException] := ( + Message[CellsToTeXException::unknownError, HoldForm[val], HoldForm[tag]]; + If[Head[val] === Failure, + val + (* else *), + $Failed + ] +) + +handleException[value_, tag_] := Throw[value, tag] + + +(* ::Subsubsection:: *) +(*catchException*) + + +SetAttributes[catchException, HoldFirst] + + +catchException[body_] := Catch[body, _CellsToTeXException, handleException] + + +(* ::Subsubsection:: *) +(*rethrowException*) + + +Options[rethrowException] = { + "TagPattern" -> _CellsToTeXException, + "AdditionalExceptionSubtypes" -> {}, + "MessageTemplate" -> Automatic, + "AdditionalMessageParameters" -> {} +} + +SetAttributes[rethrowException, HoldFirst] + + +rethrowException[rethrownBy_, opts:OptionsPattern[]] := + With[ + { + tagPattern = OptionValue["TagPattern"], + additionalExceptionSubtypes = + OptionValue["AdditionalExceptionSubtypes"], + messageTemplateRule = First @ + Options[{opts, Options[rethrowException]}, "MessageTemplate"], + additionalMessageParameters = + OptionValue["AdditionalMessageParameters"] + } + , + Function[body, + Catch[ + body + , + tagPattern + , + Function[{value, tag}, + Module[{throwInvValExc, assoc, msgParams}, + throwInvValExc = + throwException[ + rethrowException[rethrownBy, opts][body], + {"Error", "InvalidExceptionValue", #}, + HoldComplete[value] + ]&; + + If[ + !MatchQ[value, + Failure[CellsToTeXException, _Association] + ] + (* then *), + throwInvValExc["NonFailureObject"] + ]; + + assoc = Last[value]; + msgParams = + Lookup[assoc, "MessageParameters", + throwInvValExc["NoMessageParameters"] + ]; + If[!(ListQ[msgParams] && Length[msgParams > 2]) , + throwInvValExc["InvalidMessageParameters"] + ]; + + If[Last[messageTemplateRule] =!= Automatic, + assoc = Append[assoc, messageTemplateRule] + ]; + + With[ + { + newTag = Join[ + tag, + Head[tag] @@ additionalExceptionSubtypes + ] + } + , + assoc = Append[assoc, { + "MessageParameters" -> { + HoldForm[rethrownBy], + HoldForm[newTag], + Sequence @@ Drop[msgParams, 2], + Sequence @@ HoldForm /@ + additionalMessageParameters + }, + "Type" -> List @@ newTag + }]; + + Throw[Failure[CellsToTeXException, assoc], newTag] + ] + ] + ] + ] + , + HoldAll + ] + ] + + +(* ::Subsubsection:: *) +(*dataLookup*) + + +functionCall:dataLookup[ + data:{___?OptionQ}, + keys:{(_String | _Symbol)...} | _String | _Symbol +] := + Quiet[ + Check[ + OptionValue[{data}, keys] + , + With[ + { + available = + DeleteDuplicates @ Replace[ + Flatten[data][[All, 1]], + sym_Symbol :> SymbolName[sym], + {1} + ] + }, + With[ + { + missing = + Complement[ + Replace[ + Flatten[{keys}], + sym_Symbol :> SymbolName[sym], + {1} + ], + available + ] + }, + throwException[functionCall, {"Missing", "Keys"}, + {missing, available} + ] + ] + ] + , + OptionValue::optnf + ] + , + OptionValue::optnf + ] + + +(* ::Subsubsection:: *) +(*extractStyleOptions*) + + +extractStyleOptions[style_, rules:{(_Rule | _RuleDelayed)...}] := + Cases[rules, + h_[{stylePatt_ /; MatchQ[style, stylePatt], opt_}, val_] :> h[opt, val] + ] + + +(* ::Subsubsection:: *) +(*boxesToInputFormBoxes*) + + +SetAttributes[boxesToInputFormBoxes, Listable] + + +boxesToInputFormBoxes[ + str_String /; MakeExpression[str, StandardForm] === HoldComplete[Null] +] := str + +functionCall:boxesToInputFormBoxes[boxes_] := + Module[{expr, str, newBoxes}, + expr = MakeExpression[StripBoxes[boxes], StandardForm]; + If[Head[expr] =!= HoldComplete, + throwException[functionCall, {"Invalid", "Boxes"}, {boxes}] + ]; + + str = StringTake[ToString[expr, InputForm], {14, -2}]; + + newBoxes = + FrontEndExecute @ + FrontEnd`UndocumentedTestFEParserPacket[str, False]; + If[newBoxes === $Failed, + With[{str = str}, + throwException[functionCall, {"Failed", "Parser"}, + HoldComplete @ FrontEndExecute @ + FrontEnd`UndocumentedTestFEParserPacket[str, False] + ] + ] + ]; + + Replace[newBoxes, {BoxData[b_], ___} :> b] + ] + + +(* ::Subsubsection:: *) +(*boxesToString*) + + +functionCall:boxesToString[ + boxes_, boxRules_List, opts:OptionsPattern[ToString] +] := + With[{formatType = OptionValue[{opts, ToString}, "FormatType"]}, + Internal`InheritedBlock[{makeStringDefault, makeString, ToString}, + Unprotect[makeStringDefault, makeString]; + + Switch[formatType, + InputForm, + makeStringDefault[RowBox[l_List]] := makeString[l]; + makeStringDefault[str_String] := + StringReplace[ + StringTake[ToString[str], {2, -2}], + { + "\\\"" -> "\"", + "\\n" -> "\n", + "\\r" -> "\r", + "\\t" -> "\t", + "\\\\" -> "\\" + } + ]; + makeStringDefault[arg_] := ToString[arg], + OutputForm, + makeStringDefault[RowBox[l_List]] := + ToString[DisplayForm[RowBox[makeString /@ l]]]; + makeStringDefault[str_String] := + ToString[DisplayForm[RowBox[{str}]]]; + makeStringDefault[arg_] := ToString[DisplayForm[arg]], + _, + throwException[functionCall, {"Unsupported", "FormatType"}, + {formatType, {InputForm, OutputForm}} + ] + ]; + Function[{lhs, rhs}, makeString[lhs] := rhs, HoldRest] @@@ + boxRules; + SetOptions[ToString, + (* In SetOptions if option name is repeated, then last + option is used, so reverse options order. *) + Reverse @ Flatten[{opts}] + ]; + makeString[boxes] + ] + ] + + +(* ::Subsubsection:: *) +(*defaultOrFirst*) + + +defaultOrFirst[{___, default_, ___}, default_] := default + +defaultOrFirst[{first_, ___}, _] := first + + +(* ::Subsubsection:: *) +(*commonestAnnotationTypes*) + + +commonestAnnotationTypes[boxes_, allowedTypes_, specialChars : True|False] := + (* Get list of string box - syntax type pairs. *) + Cases[boxes, + syntaxBox[ + If[specialChars, + name_String + (* else *), + name_String /; + StringMatchQ[name, RegularExpression["[\\x00-\\x7F]*"]] + ], + type:allowedTypes, + ___ + ] :> + {name, type} + , + {0, Infinity} + ] // + Tally // + (* Gather tallied name - type pairs by name. *) + GatherBy[#, #[[1, 1]]&]& // + (* Convert each group to name -> commonestType rule. *) + Map[ + With[{name = #[[1, 1, 1]], typeMult = #[[All, 2]]}, + name -> + (* Select default type, if it's among commonest types, + otherwise take first of commonest types. *) + defaultOrFirst[ + (* Pick types with maximal number of occurrences.*) + Pick[#[[All, 1, 2]], typeMult, Max @ typeMult], + defaultAnnotationType[name] + ] + ]& + , + # + ]& + + + +(* ::Subsubsection:: *) +(*annotationTypesToKeyVal*) + + +annotationTypesToKeyVal[ + symToTypes:{(_Rule | _RuleDelayed)...}, + typesToKeys:{(_Rule | _RuleDelayed)...} +] := + ( + Replace[#[[1, 2]], typesToKeys] -> + StringReplace[#[[All, 1]], c:$texSpecialChars :> $texEscape <> c] + )& /@ + GatherBy[ + Cases[ + symToTypes, + (name_ -> type_) /; defaultAnnotationType[name] =!= type + ], + Last + ] + + +(* ::Subsubsection:: *) +(*annotationRulesToBoxRules*) + + +annotationRulesToBoxRules[rules:{_Rule...}] := + Replace[ + rules + , + (type_ -> {_, command_}) :> + With[ + { + start = + $commandCharsToTeX[[1, 1]] <> command <> + $commandCharsToTeX[[2, 1]], + end = $commandCharsToTeX[[3, 1]] + } + , + syntaxBox[boxes_, type, ___] :> + start <> makeString[boxes] <> end + ] + , + {1} + ] + + +(* ::Subsubsection:: *) +(*labelToCellData*) + + +labelToCellData[label_String] := + Module[{result}, + result = + StringCases[ + label + , + StartOfString ~~ "In[" ~~ i:DigitCharacter.. ~~ "]:=" ~~ + EndOfString :> + {In, ToExpression[i], None} + , + 1 + ]; + If[result === {}, + result = + StringCases[ + label + , + StartOfString ~~ "Out[" ~~ i:DigitCharacter.. ~~ "]" ~~ + ("//" ~~ form__) | "" ~~ "=" ~~ EndOfString :> + {Out, ToExpression[i], Replace[form, {"" -> None}]} + , + 1 + ] + ]; + result = Flatten[result]; + If[result === {}, + {None, None, None} + (* else *), + result + ] + ] + + +(* ::Subsubsection:: *) +(*labelToKeyVal*) + + +labelToKeyVal[ + cellLabel:(_String | None), + cellIndex:(_Integer | Automatic | None), + cellForm:(_String | Automatic | None), + currentIndex:(_Integer | Automatic) +] := + Module[ + { + label = cellLabel, index = cellIndex, form = cellForm, + labelType, labelIndex, labelForm, + addToIndex = None + } + , + If[label =!= None, + {labelType, labelIndex, labelForm} = labelToCellData[cellLabel]; + + If[labelType =!= None, + If[index === Automatic, index = labelIndex]; + If[form === Automatic, form = labelForm]; + + If[index === labelIndex && form === labelForm, + label = None + ] + ] + ]; + + If[index =!= currentIndex && IntegerQ[index] && IntegerQ[currentIndex], + addToIndex = index - currentIndex + ]; + { + DeleteCases[ + { + "label" -> label, + "addtoindex" -> addToIndex, + "form" -> form + }, + _ -> None | Automatic + ] + , + If[IntegerQ[index], + index + (* else *), + currentIndex + ] + } + ] + + +(* ::Subsubsection:: *) +(*optionValueToTeX*) + + +optionValueToTeX[True] = "true" + +optionValueToTeX[False] = "false" + +optionValueToTeX[subOpts:{OptionsPattern[]}] := optionsToTeX["{", subOpts, "}"] + +optionValueToTeX[val_] := + With[{str = ToString[val]}, + If[StringMatchQ[str, "{*}"] || StringFreeQ[str, {"[", "]", ",", "="}], + str + (* else *), + "{" <> str <> "}" + ] + ] + + +(* ::Subsubsection:: *) +(*optionsToTeX*) + + +optionsToTeX[keyval:{OptionsPattern[]}] := + StringJoin @ + Riffle[(ToString[#1] <> "=" <> optionValueToTeX[#2]) & @@@ keyval, ","] + +optionsToTeX[_String, {}, _String] := "" + +optionsToTeX[ + pre_String, keyval:{OptionsPattern[]}, post_String +] := + pre <> optionsToTeX[keyval] <> post + + +(* ::Subsubsection:: *) +(*mergeAdjacentTeXDelims*) + + +mergeAdjacentTeXDelims[startDelim_String, endDelim_String, texCode_String] := + StringReplace[texCode, { + c : Except[WordCharacter] ~~ endDelim <> startDelim :> c, + endDelim <> startDelim ~~ c : Except[WordCharacter] :> c, + endDelim ~~ ws : (" " | "\t") .. ~~ startDelim :> ws + }] + + +(* ::Subsubsection:: *) +(*mergeAdjacentTeXCommands*) + + +mergeAdjacentTeXCommands[ + cmd_String, argStart_String, argEnd_String, texCode_String +] := + StringReplace[texCode, + cmds : ( + RegularExpression[ + StringReplace[cmd, "\\" -> "\\\\"] <> "(?P" <> + argStart <> "(?:[^" <> argStart <> argEnd <> + "]|(?P>braces))*" <> argEnd <> ")" + ] ~~ (" " | "\t") ... + ) .. :> + mergeAdjacentTeXDelims[cmd <> argStart, argEnd, cmds] +] + + +(* ::Subsubsection:: *) +(*templateBoxDisplayBoxes*) + + +templateBoxDisplayBoxes[TemplateBox[boxes_, tag_, opts___]] := + Module[{displayFunction = Replace[DisplayFunction, {opts}]}, + If[displayFunction === DisplayFunction, + displayFunction = + CurrentValue[ + $currentValueObj + , + { + StyleDefinitions, + tag, + "TemplateBoxOptionsDisplayFunction" + } + ] + ]; + displayFunction @@ boxes + ] + + +(* ::Subsubsection:: *) +(*extractMessageLink*) + + +extractMessageLink[boxes_] := + FirstCase[ + System`Convert`CommonDump`RemoveLinearSyntax[ + boxes, + $convertRecursiveOption -> True + ] + , + ButtonBox[ + content_ /; + MatchQ[ + ToString @ DisplayForm[content], + ">>" | "\[RightSkeleton]" + ] + , + ___, + (Rule | RuleDelayed)[ + ButtonData, + uri_String /; StringMatchQ[uri, "paclet:ref/*"] + ], + ___ + ] :> + StringDrop[uri, 11] + , + Missing["NotFound"] + , + {0, Infinity} + ] + + +(* ::Subsubsection:: *) +(*prettifyPatterns*) + + +prettifyPatterns[expr_] := + Module[{pattNames, presentQ, duplicates, result}, + pattNames = + Alternatives @@ Cases[ + expr, + Verbatim[Pattern][name_, _] :> name, + {0, Infinity} + ]; + presentQ[_] = False; + {result, duplicates} = + Reap[expr /. name:pattNames :> + With[{nameStr = SymbolName[name]}, + If[presentQ[name], Sow[nameStr]]; + presentQ[name] = True; + nameStr /; True + ] + ]; + duplicates = Alternatives @@ Flatten[duplicates]; + + result /. Verbatim[Pattern][Except[duplicates], patt_] :> patt + ] + + +(* ::Subsubsection:: *) +(*formatToExtension*) + + +formatToExtension[_String] = "" + +Scan[ + (formatToExtension[Last[#]] = + StringDrop[First[#], 1]) & + , + GatherBy[System`ConvertersDump`$extensionMappings, Last][[All, 1]] +] +Scan[ + With[{ext = formatToExtension[Last[#]]}, + If[ext =!= "", + formatToExtension[First[#]] = ext + ] + ] & + , + System`ConvertersDump`$formatMappings +] + + +(* ::Subsubsection:: *) +(*$convertRecursiveOption*) + + +$convertRecursiveOption = + Options[System`Convert`CommonDump`RemoveLinearSyntax][[1, 1]] + + +(* ::Subsection:: *) +(*Configuration*) + + +(* ::Subsubsection:: *) +(*$supportedCellStyles*) + + +$supportedCellStyles = "Code" | "Input" | "Output" | "Print" | "Message" + + +(* ::Subsubsection:: *) +(*$cellStyleOptions*) + + +$cellStyleOptions = { + {"Code", "Processor"} -> + Composition[ + trackCellIndexProcessor, mmaCellProcessor, boxesToTeXProcessor, + boxRulesProcessor, annotateSyntaxProcessor, toInputFormProcessor, + cellLabelProcessor, extractCellOptionsProcessor + ], + {"Input", "Processor"} -> + Composition[ + trackCellIndexProcessor, mmaCellProcessor, boxesToTeXProcessor, + boxRulesProcessor, annotateSyntaxProcessor, cellLabelProcessor, + extractCellOptionsProcessor + ], + {"Output" | "Print", "Processor"} -> + Composition[ + trackCellIndexProcessor, mmaCellProcessor, boxesToTeXProcessor, + boxRulesProcessor, cellLabelProcessor, extractCellOptionsProcessor + ], + {"Message", "Processor"} -> + Composition[ + trackCellIndexProcessor, mmaCellProcessor, boxesToTeXProcessor, + boxRulesProcessor, messageLinkProcessor, cellLabelProcessor, + extractCellOptionsProcessor + ], + {"Code", "BoxRules"} :> $linearBoxesToTeX, + {"Input" | "Output" | "Print" | "Message", "BoxRules"} :> + Join[ + $linearBoxesToTeX, + $boxesToFormattedTeX, + headRulesToBoxRules[$boxHeadsToTeXCommands] + ], + {"Input" | "Output" | "Print" | "Message", "StringRules"} :> + Join[$stringsToTeX, $commandCharsToTeX], + {"Input", "NonASCIIHandler"} -> (charToTeX[#, FontWeight -> Bold]&), + {"Output" | "Print" | "Message", "NonASCIIHandler"} -> + (charToTeX[#, FontWeight -> Plain]&), + {"Code", "CharacterEncoding"} -> "ASCII", + {"Input" | "Output" | "Print" | "Message", "CharacterEncoding"} -> + "Unicode", + {"Code" | "Input", "FormatType"} -> InputForm, + {"Output" | "Print" | "Message", "FormatType"} -> OutputForm, + {"Input", "TeXCodeSimplifier"} -> + (mergeAdjacentTeXCommands[ + $commandCharsToTeX[[1, 1]] <> "pmb", + $commandCharsToTeX[[2, 1]], + $commandCharsToTeX[[3, 1]], + mergeAdjacentTeXDelims[ + $commandCharsToTeX[[1, 1]] <> "(", + $commandCharsToTeX[[1, 1]] <> ")", + # + ] + ]&), + {"Output" | "Print" | "Message", "TeXCodeSimplifier"} -> + (mergeAdjacentTeXDelims[ + $commandCharsToTeX[[1, 1]] <> "(", + $commandCharsToTeX[[1, 1]] <> ")", + # + ]&), + {"Code" | "Input" | "Output", "Indexed"} -> True, + {"Print" | "Message", "Indexed"} -> False, + {"Code" | "Input", "Intype"} -> True, + {"Output" | "Print" | "Message", "Intype"} -> False, + {"Print" | "Message", "CellLabel"} -> None +} + + +(* ::Subsubsection:: *) +(*$commandCharsToTeX*) + + +$commandCharsToTeX = { + "\\" -> "\\textbackslash{}", + "{" -> "\\{", + "}" -> "\\}" +} + + +(* ::Subsubsection:: *) +(*$basicBoxes*) + + +$basicBoxes = _BoxData | _TextData | _RowBox | _String | _List + + +(* ::Subsubsection:: *) +(*$linearBoxesToTeX*) + + +$linearBoxesToTeX = { + RowBox[l_List] :> makeString[l] + , + (StyleBox | ButtonBox | InterpretationBox | FormBox | TagBox | TooltipBox)[ + contents_, ___ + ] :> makeString[contents] + , + tb:TemplateBox[_, _, ___] :> makeString[templateBoxDisplayBoxes[tb]] + , + GridBox[grid:{___List}, ___] :> + StringJoin@Riffle[Riffle[makeString /@ #, "\t"]& /@ grid, "\n"] + , + PaneSelectorBox[ + rules:{(_Rule | _RuleDelayed)...}, + HoldPattern[Dynamic][v_, ___] | v_, + Shortest[def_:" "], + OptionsPattern[] + ] :> + makeString@Replace[v, Append[rules, _ :> def]] +} + + +(* ::Subsubsection:: *) +(*$boxesToFormattedTeX*) + + +$boxesToFormattedTeX = + ( + #1["\[Integral]", scr_, OptionsPattern[]] :> + With[ + { + escChar = $commandCharsToTeX[[1, 1]], + argStart = $commandCharsToTeX[[2, 1]], + argEnd = $commandCharsToTeX[[3, 1]] + } + , + StringJoin[ + escChar, #2, + argStart, escChar, "int", argEnd, + argStart, makeString[scr], argEnd + ] + ] + )& @@@ { + SubscriptBox -> "mmaSubM", + SuperscriptBox -> "mmaSupM" + } + +AppendTo[$boxesToFormattedTeX, + SubsuperscriptBox["\[Integral]", sub_, sup_, OptionsPattern[]] :> + With[ + { + escChar = $commandCharsToTeX[[1, 1]], + argStart = $commandCharsToTeX[[2, 1]], + argEnd = $commandCharsToTeX[[3, 1]] + } + , + StringJoin[ + escChar, "mmaSubSupM", + argStart, escChar, "int", argEnd, + argStart, makeString[sub], argEnd, + argStart, makeString[sup], argEnd + ] + ] +] + + +(* ::Subsubsection:: *) +(*$boxHeadsToTeXCommands*) + + +$boxHeadsToTeXCommands = { + SubscriptBox -> {"mmaSub", 2}, + SuperscriptBox -> {"mmaSup", 2}, + SubsuperscriptBox -> {"mmaSubSup", 3}, + UnderscriptBox -> {"mmaUnder", 2}, + OverscriptBox -> {"mmaOver", 2}, + UnderoverscriptBox -> {"mmaUnderOver", 3}, + FractionBox -> {"mmaFrac", 2}, + SqrtBox -> {"mmaSqrt", 1}, + RadicalBox -> {"mmaRadical", 2} +} + + +(* ::Subsubsection:: *) +(*$stringsToTeX*) + + +$stringsToTeX = { + "\[LeftSkeleton]" -> "<<", + "\[RightSkeleton]" -> ">>" +} + +If[$VersionNumber >=10, + $stringsToTeX = + Join[ + { + ToExpression["\"\\[LeftAssociation]\""] -> "<|", + ToExpression["\"\\[RightAssociation]\""] -> "|>" + }, + $stringsToTeX + ] +] + + +(* ::Subsubsection:: *) +(*$annotationTypesToTeX*) + + +$annotationTypesToTeX = { + "DefinedSymbol" -> {"defined", "mmaDef"}, + "UndefinedSymbol" -> {"undefined", "mmaUnd"}, + "LocalVariable" -> {"local", "mmaLoc"}, + "FunctionLocalVariable" -> {"functionlocal", "mmaFnc"}, + "PatternVariable" -> {"pattern", "mmaPat"}, + "LocalScopeConflict" -> {"localconflict", "mmaLCn"}, + "GlobalToLocalScopeConflict" -> {"globalconflict", "mmaGCn"}, + "ExcessArgument" -> {"excessargument", "mmaExc"}, + "UnknownOption" -> {"unknownoption", "mmaOpt"}, + "UnwantedAssignment" -> {"unwantedassignment", "mmaAsg"}, + "SymbolShadowing" -> {"shadowing", "mmaShd"}, + "SyntaxError" -> {"syntaxerror", "mmaSnt"}, + "EmphasizedSyntaxError" -> {"emphasizedsyntaxerror", "mmaEmp"}, + "FormattingError" -> {"formattingerror", "mmaFmt"}, + "String" -> {None, "mmaStr"}, + "Comment" -> {None, "mmaCmt"} +} + + +(* ::Subsubsection:: *) +(*$currentValueObj*) + + +$currentValueObj = Sequence[] + + +(* ::Subsubsection:: *) +(*makeString*) + + +makeString[arg_] := makeStringDefault[arg] + +makeString[ + str_String?System`Convert`CommonDump`EmbeddedStringWithLinearSyntaxQ +] := + makeString @ System`Convert`CommonDump`removeLinearSyntax[ + str, + $convertRecursiveOption -> True + ] + + +(* ::Subsubsection:: *) +(*makeStringDefault*) + + +makeStringDefault["\[IndentingNewLine]"] := "\n" + +makeStringDefault[boxes_List] := StringJoin[makeString /@ boxes] + +makeStringDefault[BoxData[boxes_]] := makeString[boxes] + + +(* ::Subsubsection:: *) +(*headRulesToBoxRules*) + + +SetAttributes[headRulesToBoxRules, Listable] + + +headRulesToBoxRules[ + boxHead_ -> {texCommandName_String, argsNo_Integer?NonNegative} +] := + With[ + { + comm = $commandCharsToTeX[[1, 1]] <> texCommandName, + argStart = $commandCharsToTeX[[2, 1]], + argEnd = $commandCharsToTeX[[3, 1]] + } + , + HoldPattern @ boxHead[boxes:Repeated[_, {argsNo}], OptionsPattern[]] :> + comm <> (argStart <> makeString[#] <> argEnd& /@ {boxes}) + ] + + +(* ::Subsubsection:: *) +(*defaultAnnotationType*) + + +defaultAnnotationType[ + sym:(_Symbol | _String) /; + Quiet[Context[sym], Context::notfound] === "System`" +] := "DefinedSymbol" + +defaultAnnotationType[_Symbol | _String] := "UndefinedSymbol" + + +(* ::Subsubsection:: *) +(*texMathReplacementRegister*) + + +texMathReplacementRegister[str_String] := ( + If[! StringQ @ texMathReplacement[str], + With[{texCode = StringTrim @ System`Convert`TeXFormDump`MakeTeX[str]}, + If[texCode =!= "", + texMathReplacement[str] = texCode + ] + ] + ]; + str +) + + +(* ::Subsubsection:: *) +(*charToTeX*) + + +Options[charToTeX] = {FontWeight -> Plain} + + +functionCall:charToTeX[char_, OptionsPattern[]] := + With[ + { + styleWrapper = + Replace[OptionValue[FontWeight], { + Plain -> Identity, + Bold -> ({"\\pmb{", # , "}"} &), + _ :> + throwException[functionCall, + {"Unsupported", "OptionValue", FontWeight}, + {OptionValue[FontWeight], {Plain, Bold}} + ] + }] + } + , + StringReplace[ + StringJoin @ Replace[ + System`Convert`TeXFormDump`TextExceptions @ + System`Convert`TeXFormDump`TeXCharacters[char] + , + { + {"$", texStr_, "$"} :> + If[StringFreeQ[texStr, "\\"], + texStr + (* else *), + {"\\(", styleWrapper[texStr], "\\)"} + ], + texStr_String /; !StringFreeQ[texStr, "\\"] :> + styleWrapper[texStr] + } + ] + , + With[{escChar = $commandCharsToTeX[[1, 1]]}, + { + " " -> "", + "\\{" -> escChar <> "{", "{" -> $commandCharsToTeX[[2, 1]], + "\\}" -> escChar <> "}", "}" -> $commandCharsToTeX[[3, 1]], + "\\\\" -> "\\\\", "\\" -> escChar + } + ] + ] + ] + + +(* ::Subsubsection:: *) +(*fileNameGenerator*) + + +Options[fileNameGenerator] = { + "CellOptionsFilter" -> Except[{CellLabel, CellChangeTimes}] +} + + +fileNameGenerator[boxes_, exportFormat_String, OptionsPattern[]] := + IntegerString[ + Hash[ + Replace[boxes, + Cell[contents__, Longest[opts__?OptionQ]] :> + Cell[contents, + Sequence @@ FilterRules[ + {opts}, OptionValue["CellOptionsFilter"] + ] + ] + ], + "MD5" + ], + 16, + 8 + ] <> + formatToExtension[exportFormat] + + +(* ::Subsubsection:: *) +(*processorDataLookup*) + + +SetAttributes[processorDataLookup, HoldFirst] + + +processorDataLookup[ + functionCall_, + data:{___?OptionQ}, + keys:{(_String | _Symbol)...} | _String | _Symbol +] := + rethrowException[functionCall, + "TagPattern" -> CellsToTeXException["Missing", "Keys"], + "AdditionalExceptionSubtypes" -> {"ProcessorArgument"}, + "MessageTemplate" :> CellsToTeXException::missingProcArg + ] @ dataLookup[data, keys] + + +(* ::Subsubsection:: *) +(*extractCellOptionsProcessor*) + + +functionCall:extractCellOptionsProcessor[data:{___?OptionQ}] := + With[ + { + cellOpts = + Cases[ + processorDataLookup[functionCall, data, "Boxes"], + Cell[__, Longest[cellOpts___?OptionQ]] :> cellOpts, + {0}, + 1 + ] + } + , + If[cellOpts === {}, + data + (* else *), + {data, cellOpts} + ] + ] + + +(* ::Subsubsection:: *) +(*cellLabelProcessor*) + + +Options[cellLabelProcessor] = { + "Indexed" -> False, + "Intype" -> False, + "CellLabel" -> None, + "CellIndex" -> Automatic, + "CellForm" -> Automatic +} + + +functionCall:cellLabelProcessor[data:{___?OptionQ}] := + Module[ + { + texOptions, texOptionsFromLabel, + indexed, intype, cellLabel, cellIndex, cellForm, + previousIntype, currentCellIndex + } + , + { + texOptions, currentCellIndex, previousIntype, + indexed, intype, cellLabel, cellIndex, cellForm + } = + processorDataLookup[functionCall, + {data, Options[cellLabelProcessor]}, + { + "TeXOptions", "CurrentCellIndex", "PreviousIntype", + "Indexed", "Intype", "CellLabel", "CellIndex", "CellForm" + } + ]; + + If[indexed && (intype || ! previousIntype) && + IntegerQ[currentCellIndex] + , + currentCellIndex++ + ]; + + {texOptionsFromLabel, currentCellIndex} = + labelToKeyVal[cellLabel, cellIndex, cellForm, currentCellIndex]; + + texOptions = Join[texOptionsFromLabel, texOptions]; + + { + "TeXOptions" -> texOptions, + "Indexed" -> indexed, + "Intype" -> intype, + "CellIndex" -> currentCellIndex, + data + } + ] + + +(* ::Subsubsection:: *) +(*trackCellIndexProcessor*) + + +functionCall:trackCellIndexProcessor[data:{___?OptionQ}] := + Module[{indexed, cellIndex, intype}, + {indexed, cellIndex, intype} = + processorDataLookup[functionCall, + data, {"Indexed", "CellIndex", "Intype"} + ]; + + If[indexed, + SetOptions[CellToTeX, + "CurrentCellIndex" -> cellIndex, + "PreviousIntype" -> intype + ] + ]; + + data + ] + + +(* ::Subsubsection:: *) +(*annotateSyntaxProcessor*) + + +Options[annotateSyntaxProcessor] = { + "BoxRules" -> {}, + "AnnotationTypesToTeX" :> $annotationTypesToTeX, + "CommonestTypesAsTeXOptions" -> "ASCII", + "StringBoxToTypes" -> Automatic, + "AnnotateComments" -> Automatic +} + + +functionCall:annotateSyntaxProcessor[data:{___?OptionQ}] := + Module[ + { + boxes, boxRules, texOptions, + annotationTypesToTeX, commonestTypesAsTeXOptions, + stringBoxToTypes, annotateComments, + preprocessedBoxes, commonestTypes, annotationTypesToTeXKeys + } + , + { + boxes, boxRules, texOptions, + annotationTypesToTeX, commonestTypesAsTeXOptions, + stringBoxToTypes, annotateComments + } = + processorDataLookup[functionCall, + {data, Options[annotateSyntaxProcessor]}, + { + "Boxes", "BoxRules", "TeXOptions", + "AnnotationTypesToTeX", "CommonestTypesAsTeXOptions", + "StringBoxToTypes", "AnnotateComments" + } + ]; + + If[stringBoxToTypes === Automatic, + stringBoxToTypes = + { + If[commonestTypesAsTeXOptions === False, + Automatic + (* else *), + (* Delete String type annotation rule. *) + Delete[CellsToTeX`SyntaxAnnotations`Private`$stringBoxToTypes, 2] + ] + , + _String?CellsToTeX`SyntaxAnnotations`Private`symbolNameQ -> + {"DefinedSymbol"} + } // Flatten + ]; + If[annotateComments === Automatic, + annotateComments = commonestTypesAsTeXOptions === False + ]; + + preprocessedBoxes = + AnnotateSyntax[ + boxes, + "Annotation" -> (syntaxBox[#1, First@#2]&), + "StringBoxToTypes" -> stringBoxToTypes, + "AnnotateComments" -> annotateComments + ]; + + Switch[commonestTypesAsTeXOptions, + "ASCII" | True, + commonestTypes = + commonestAnnotationTypes[ + preprocessedBoxes, + Except[ + Alternatives @@ Cases[annotationTypesToTeX, + _[type_, {None, _}] :> type + ] + ], + TrueQ[commonestTypesAsTeXOptions] + ]; + + preprocessedBoxes = + preprocessedBoxes /. + (syntaxBox[#1, #2] :> #1 & @@@ commonestTypes); + + annotationTypesToTeXKeys = + Cases[annotationTypesToTeX, + h_[type_, {key:Except[None], _}] :> + h[type, "more" <> key] + ]; + + texOptions = + Join[texOptions, + annotationTypesToKeyVal[ + commonestTypes + , + Append[annotationTypesToTeXKeys, + unsupportedType_ :> + throwException[functionCall, + {"Unsupported", "AnnotationType"}, + { + unsupportedType, + annotationTypesToTeXKeys[[All, 1]] + } + ] + ] + ] + ] + , + Except[False], + throwException[functionCall, + {"Unsupported", "OptionValue", "CommonestTypesAsTeXOptions"}, + {commonestTypesAsTeXOptions, {True, "ASCII", False}} + ] + ]; + + { + "Boxes" -> preprocessedBoxes, + "BoxRules" -> + Join[ + annotationRulesToBoxRules[annotationTypesToTeX], + boxRules + ], + "TeXOptions" -> texOptions, + data + } + ] + + +(* ::Subsubsection:: *) +(*toInputFormProcessor*) + + +functionCall:toInputFormProcessor[data:{___?OptionQ}] := + Module[{boxes, template, placeholder}, + boxes = processorDataLookup[functionCall, data, "Boxes"]; + template = + Replace[boxes, { + Cell[BoxData[b_], cellData___] :> ( + boxes = b; + Cell[BoxData[placeholder], cellData] + ), + Cell[b_, cellData___] :> ( + boxes = b; + Cell[placeholder, cellData] + ), + BoxData[b_] :> ( + boxes = b; + BoxData[placeholder] + ), + _ -> placeholder + }]; + + boxes = + rethrowException[functionCall, + "TagPattern" -> + CellsToTeXException["Invalid", "Boxes"] | + CellsToTeXException["Failed", "Parser"] + ] @ + boxesToInputFormBoxes[boxes]; + boxes = template /. placeholder -> boxes; + + {"Boxes" -> boxes, data} + ] + + +(* ::Subsubsection:: *) +(*messageLinkProcessor*) + + +functionCall:messageLinkProcessor[data:{___?OptionQ}] := + Module[{boxes, texOptions, messageLink}, + {boxes, texOptions} = + processorDataLookup[functionCall, data, {"Boxes", "TeXOptions"}]; + + messageLink = extractMessageLink[boxes]; + + If[MatchQ[messageLink, _Missing], + data + (* else *), + AppendTo[texOptions, "messagelink" -> messageLink]; + {"TeXOptions" -> texOptions, data} + ] + ] + + +(* ::Subsubsection:: *) +(*boxRulesProcessor*) + + +Options[boxRulesProcessor] = { + "BoxRules" -> {}, "StringRules" -> {}, "NonASCIIHandler" -> Identity +} + + +functionCall:boxRulesProcessor[data:{___?OptionQ}] := + Module[{boxRules, stringRules, nonASCIIHandler}, + {boxRules, stringRules, nonASCIIHandler} = + processorDataLookup[functionCall, + {data, Options[boxRulesProcessor]}, + {"BoxRules", "StringRules", "NonASCIIHandler"} + ]; + + If[nonASCIIHandler =!= Identity, + With[{nonASCIIHandler = nonASCIIHandler}, + AppendTo[stringRules, + char:RegularExpression["[^\\x00-\\x7F]"] :> + nonASCIIHandler[char] + ] + ] + ]; + + If[stringRules =!= {}, + With[{stringRules = stringRules}, + AppendTo[boxRules, + str_String :> + StringReplace[makeStringDefault[str], stringRules] + ] + ] + ]; + + {"BoxRules" -> boxRules, "StringRules" -> stringRules, data} + ] + + +(* ::Subsubsection:: *) +(*boxesToTeXProcessor*) + + +Options[boxesToTeXProcessor] = + {"BoxRules" -> {}, "TeXCodeSimplifier" -> Identity} + + +functionCall:boxesToTeXProcessor[data:{___?OptionQ}] := + Module[{boxes, boxRules, texCodeSimplifier, texCode}, + {boxes, boxRules, texCodeSimplifier} = + processorDataLookup[functionCall, + {data, Options[boxesToTeXProcessor]}, + {"Boxes", "BoxRules", "TeXCodeSimplifier"} + ]; + boxes = Replace[boxes, Cell[contents_, ___] :> contents]; + boxes = Replace[boxes, BoxData[b_] :> b]; + + AppendTo[boxRules, + With[{supportedBoxes = boxRules[[All, 1]]}, + unsupportedBox:Except[$basicBoxes] :> + throwException[functionCall, {"Unsupported", "Box"}, + {unsupportedBox, supportedBoxes} + ] + ] + ]; + + texCode = + texCodeSimplifier @ + rethrowException[functionCall, + "TagPattern" -> + CellsToTeXException["Unsupported", "FormatType"] + ] @ boxesToString[ + boxes, boxRules, FilterRules[data, Options[ToString]] + ]; + + {"TeXCode" -> texCode, data} + ] + + +(* ::Subsubsection:: *) +(*mmaCellProcessor*) + + +Options[mmaCellProcessor] = {"Indentation" -> " "} + + +functionCall:mmaCellProcessor[data:{___?OptionQ}] := + Module[{texCode, style, texOptions, indentation}, + {texCode, style, texOptions, indentation} = + processorDataLookup[functionCall, + {data, Options[mmaCellProcessor]}, + {"TeXCode", "Style", "TeXOptions", "Indentation"} + ]; + + texCode = StringJoin[ + "\\begin{mmaCell}", + optionsToTeX["[", texOptions, "]"], + "{", style, "}", + StringReplace[ + StringJoin["\n", texCode], + "\n" | "\[IndentingNewLine]" -> "\n" <> indentation + ] + , + "\n\\end{mmaCell}" + ]; + + {"TeXCode" -> texCode, data} + ] + + +(* ::Subsubsection:: *) +(*exportProcessor*) + + +Options[exportProcessor] = { + "AdditionalCellOptions" -> {ShowSyntaxStyles -> True}, + "ExportFormat" -> "PDF", + "FileNameGenerator" -> fileNameGenerator +} + + +functionCall:exportProcessor[data:{___?OptionQ}] := + Module[ + { + boxes, style, additionalCellOptions, exportFormat, nameGenerator, + filename, exportResult + } + , + {boxes, style, additionalCellOptions, exportFormat, nameGenerator} = + processorDataLookup[functionCall, + {data, Options[exportProcessor]}, + { + "Boxes", "Style", "AdditionalCellOptions", "ExportFormat", + "FileNameGenerator" + } + ]; + + additionalCellOptions = Sequence @@ additionalCellOptions; + boxes = + Replace[boxes, { + Cell[contents_, rest___, Longest[cellOpts___?OptionQ]] :> + Cell[ + contents, style, rest, additionalCellOptions, cellOpts + ] + , + contents:(_BoxData | _TextData) :> + Cell[contents, style, additionalCellOptions] + , + other_ :> Cell[BoxData[other], style, additionalCellOptions] + }]; + + filename = nameGenerator[boxes, exportFormat]; + exportResult = Export[filename, boxes, exportFormat, data]; + + If[exportResult === $Failed, + With[ + { + filename = filename, boxes = boxes, + exportFormat = exportFormat + } + , + throwException[functionCall, {"Failed", "Export"}, + HoldComplete[Export[filename, boxes, exportFormat, data]] + ] + ] + ]; + + {"FileName" -> exportResult, data} + ] + + +(* ::Subsubsection:: *) +(*mmaCellGraphicsProcessor*) + + +functionCall:mmaCellGraphicsProcessor[data:{___?OptionQ}] := + Module[ + {style, texOptions, filename, texCode} + , + {filename, style, texOptions} = + processorDataLookup[functionCall, + data, {"FileName", "Style", "TeXOptions"} + ]; + + texCode = StringJoin[ + "\\mmaCellGraphics", optionsToTeX["[", texOptions, "]"], + "{", style, "}{", filename, "}" + ]; + + {"TeXCode" -> texCode, data} + ] + + +(* ::Subsubsection:: *) +(*Common operations*) + + +Protect @ Evaluate @ DeleteCases[ + Names["CellsToTeX`Configuration`" ~~ Except["$"] ~~ Except["`"] ...], + "texMathReplacement", + {1}, + 1 +] + + +(* ::Subsection:: *) +(*Public*) + + +(* ::Subsubsection:: *) +(*CellToTeX*) + + +Options[CellToTeX] = { + "Style" -> Automatic, + "SupportedCellStyles" :> $supportedCellStyles, + "CellStyleOptions" :> $cellStyleOptions, + "ProcessorOptions" -> {}, + "Processor" -> Composition[mmaCellGraphicsProcessor, exportProcessor], + "TeXOptions" -> {}, + "CatchExceptions" -> True, + "CurrentCellIndex" -> Automatic, + "PreviousIntype" -> False +} + + +functionCall:CellToTeX[boxes_, opts:OptionsPattern[]] := + If[OptionValue["CatchExceptions"], + catchException + (* else *), + Identity + ] @ Module[ + { + style, supportedCellStyles, processorOpts, cellStyleOptions, + styleOptions, data, processor + } + , + {style, supportedCellStyles, processorOpts, cellStyleOptions} = + OptionValue[{ + "Style", "SupportedCellStyles", "ProcessorOptions", + "CellStyleOptions" + }]; + + If[style === Automatic, + Replace[boxes, + Cell[_, cellStyle_String, ___] :> (style = cellStyle;) + ]; + If[style === Automatic, + throwException[functionCall, {"Missing", "CellStyle"}, + {boxes} , "missingCellStyle" + ] + ] + ]; + + If[!MatchQ[style, supportedCellStyles], + throwException[functionCall, {"Unsupported", "CellStyle"}, + {style, supportedCellStyles} + ] + ]; + + styleOptions = extractStyleOptions[style, cellStyleOptions]; + + data = { + "Boxes" -> boxes, + "Style" -> style, + opts, + processorOpts, + styleOptions, + Options[CellToTeX] + }; + processor = dataLookup[data, "Processor"]; + data = processor[data]; + + rethrowException[functionCall, + "TagPattern" -> CellsToTeXException["Missing", "Keys"], + "AdditionalExceptionSubtypes" -> {"ProcessorResult"}, + "MessageTemplate" :> CellsToTeXException::missingProcRes, + "AdditionalMessageParameters" -> {processor} + ] @ dataLookup[data, "TeXCode"] + ] + + +(* ::Subsubsection:: *) +(*CellsToTeXPreamble*) + + +Options[CellsToTeXPreamble] = { + "Gobble" -> Automatic, + "UseListings" -> Automatic, + "TeXOptions" -> {}, + "TeXMathReplacement" -> texMathReplacement, + "CatchExceptions" -> True +} + + +functionCall:CellsToTeXPreamble[OptionsPattern[]] := + If[OptionValue["CatchExceptions"], + catchException + (* else *), + Identity + ] @ Module[ + {gobble, useListings, texOptions, mathReplacement} + , + {gobble, useListings, texOptions, mathReplacement} = + OptionValue @ + {"Gobble", "UseListings", "TeXOptions", "TeXMathReplacement"}; + + If[!MatchQ[texOptions, {OptionsPattern[]}], + throwException[functionCall, + {"Unsupported", "OptionValue", "TeXOptions"}, + {texOptions, {"list of options"}} + ] + ]; + If[!MatchQ[mathReplacement, Except[HoldPattern@Symbol[___], _Symbol]], + throwException[functionCall, + {"Unsupported", "OptionValue", "TeXMathReplacement"}, + {mathReplacement, {"a symbol"}} + ] + ]; + + (* By default gobble default indentation of mmaCell. *) + If[gobble === Automatic, + gobble = + StringLength @ OptionValue[mmaCellProcessor, "Indentation"] + ]; + Switch[gobble, + _Integer?NonNegative, + PrependTo[texOptions, "morefv" -> {"gobble" -> gobble}] + , + Except[None], + throwException[functionCall, + {"Unsupported", "OptionValue", "Gobble"}, + {gobble, {Automatic, None, "non-negative integer"}} + ] + ]; + + (* Listings are used to highlight non-annotated code using TeX + environment options. If moving annotations to options is switched + off then, by default, don't use listings, otherwise use them. + Since listings are, by default, switched on in mmacells package, + if we're using them, we can just omit this option. *) + If[useListings === Automatic, + useListings = + Replace[ + OptionValue[ + annotateSyntaxProcessor, + "CommonestTypesAsTeXOptions" + ], + Except[False] -> None + ] + ]; + Switch[useListings, + True | False, + PrependTo[texOptions, "uselistings" -> useListings] + , + Except[None], + throwException[functionCall, + {"Unsupported", "OptionValue", "UseListings"}, + {useListings, {Automatic, True, False, None}} + ] + ]; + StringJoin @ Riffle[ + DeleteCases[ + Prepend[ + StringJoin[ + "\\mmaDefineMathReplacement{", + #1[[1, 1]], + "}{", + #2, + "}" + ]& @@@ + DownValues @ Evaluate[mathReplacement] + , + optionsToTeX["\\mmaSet{", texOptions, "}"] + ], + "" + ], + "\n" + ] + ] + + +(* ::Subsubsection:: *) +(*Common operations*) + + +Protect @ Evaluate @ Names[ + "CellsToTeX`" ~~ Except["$"] ~~ Except["`"]... +] + + +End[] + + +EndPackage[] diff --git a/mathics/packages/CellsToTeX/Kernel/init.m b/mathics/packages/CellsToTeX/Kernel/init.m new file mode 100644 index 000000000..6c252ebd3 --- /dev/null +++ b/mathics/packages/CellsToTeX/Kernel/init.m @@ -0,0 +1,10 @@ +(* + +This loads the package CellsToTeX: + +https://github.com/jkuczm/MathematicaCellsToTeX + +Right now, it is in a very experimental stage. +*) + +< +Released under The MIT License +https://github.com/jkuczm/MathematicaSyntaxAnnotations/blob/master/LICENSE *) + + +(* Begining of package context with ` is intentionall it allows loading + SyntaxAnnotations as a sub-package. Apropriate steps to load package with + SyntaxAnnotations` as "top level" context are taken in Kernel/init.m. + This is inspired by system used in https://github.com/szhorvat/LTemplate *) +BeginPackage["`SyntaxAnnotations`"] + + +Unprotect["`*"] +ClearAll["`*"] + + +(* ::Section:: *) +(*Usage messages*) + + +AnnotateSyntax::usage = +"\ +AnnotateSyntax[boxes] \ +returns boxes with certain subboxes wrapped with boxes identifying their \ +syntactic role." + + +(* ::Section:: *) +(*Implementation*) + + +Begin["`Private`"] + + +ClearAll["`*"] + + +(* ::Subsection:: *) +(*Private symbols usage*) + + +undefinedSymbolQ::usage = +"\ +undefinedSymbolQ[symbol] \ +returns True if symbol is undefined. Returns False otherwise.\ + +undefinedSymbolQ[\"name\"] \ +returns True if symbol, with given \"name\", is undefined. Returns False \ +otherwise." + + +symbolNameQ::usage = +"\ +symbolNameQ[\"str\"] \ +returns True if given String is valid symbol name, returns False otherwise." + + +whitespaceQ::usage = +"\ +whitespaceQ[\"str1\", \"str2\", ...] \ +returns True if given strings are empty or contain only whitespace or \ +\\[IndentingNewLine] characters, returns False otherwise." + + +syntaxBox::usage = +"\ +syntaxBox[boxes, {{type1, subtype1, ...}, {type2, subtype2, ...}, ...}] \ +represents boxes that in an expression perform sytnax roles of given types." + + +extractSymbolName::usage = +"\ +extractSymbolName[\"str\"] \ +returns a List. For \"str\" being valid symbol name returned list conatins \ +this name. For box representation of Blank... pattern with head, returned \ +list contains this head. For box representation of pattern with name, \ +returned list contains this name. If none of above is true returned list is \ +empty." + + +extractLocalVariableNames::usage = +"\ +extractLocalVariableNames[\"type\"][boxes1, boxes2, ...] \ +returns a List of strings with names of all local symbols extracted from \ +given boxes. \ +\"type\" can be any of types from \"LocalVariables\" property of \ +SyntaxInformation, with addition of: \ +\"Scoping\" type, applicable to With, Module and Block; \ +\"LowerBound\" applicable to \[Sum] and \[Product]; \ +\"IntegrateDifferentialD\" applicable to \[Integral]; \ +\"PatternName\" for rules and set expressions. \ +boxesi are treated as box representation of function arguments, they don't \ +need to represent valid mathematica expressions." + + +extractArgs::usage = +"\ +extractArgs[boxes, {imin, imax}] \ +returns List of box representations of function arguments, with positions \ +from imin to imax, extracted from boxes treated as representation of sequence \ +of arguments.\ + +extractArgs[boxes, {i}] \ +returns List containing one element: box representation of function argument, \ +with position i.\ + +extractArgs[boxes, 0] \ +returns List containing given boxes.\ + +boxes don't need to represent valid mathematica expression. \ +Second argument of extractArgs accepts same values as second element of \ +\"LocalVariables\" property of SyntaxInformation with addition of 0. \ +All occurrences of syntaxBox are stripped." + + +extendedSyntaxInformation::usage = +"\ +extendedSyntaxInformation[\"name\"] \ +returns SyntaxInformation for symbol with given \"name\". \ +extendedSyntaxInformation for some built-in symbols is enriched with \ +properties that are missing from ordinary SyntaxInformation, yet equivalent \ +functionality is implemented by other means.\ + +extendedSyntaxInformation[\"op1\", \"op2\"] \ +returns SyntaxInformation for operators composed of two parts." + + +$inFunction::usage = +"\ +$inFunction \ +is True if parser is currently inside box expression representing \ +one-argument Function, otherwise it's false." + + +$directlyInScopingRHS::usage = +"\ +$directlyInScopingRHS \ +is True if parser is currently directly inside box expression representing \ +right hand side of assignment in variable specification of With, Module or +Block, otherwise it's false." + + +patternNameTypes::usage = +"\ +patternNameTypes[name] \ +returns List of syntax types assigned to pattern with given name. DownValues \ +of this function are changed during parsing." + + +stringBoxTypes::usage = +"\ +stringBoxTypes[str] \ +returns List of syntax types assigned to given box String str. DownValues \ +of this function are changed during parsing." + + +deafultStringBoxTypes::usage = +"\ +deafultStringBoxTypes[str] \ +returns List of syntax types assigned to given box String str, excluding \ +contex dependend types assigned during parsing." + + +modifyTypes::usage = +"\ +modifyTypes[register, type, symNames] \ +adds given type to syntax types of given symbol names symNames in given \ +register. Possible registers are patternNameTypes and stringBoxTypes.\ + +modifyTypes[{register1, register2, ...}, type, symNames] \ +registers types in all given registers." + + +withModifiedTypes::usage = +"\ +withModifiedTypes[{type, symNames}, body] \ +evaluates body with with given syntax type added to types, of given symbol \ +names symNames, in patternNameTypes and stringBoxTypes registers. + +withModifiedTypes[{register1, register2, ...}, {type, symNames}, body] \ +registers changes in all given registers." + + +withLocalVariables::usage = +"\ +withLocalVariables[{funcName, localVarsSym, argumentBoxes}, body] \ +when used on right hand side of SetDelayed assigns to left hand side given \ +body wrapped with condition that causes definition to apply only when given \ +funcName has defined extendedSyntaxInformation with \"LocalVariables\" \ +element. body can contain symbol given as localVarsSym and it will be \ +replaced by list of box representations of local variables extracted from \ +argumentBoxes." + + +parseScopingVars::usage = +"\ +parseScopingVars[mode, funcName, localVars][boxes] \ +returns given boxes with symbol names, given in localVars List, wrapped with \ +syntaxBox identifying their role as local variables of scoping construct. \ +Appearances of symbol names, from localVars, on right hand side of \ +assignments present directly in given boxes are annotated differently than \ +other appearances of those symbol names." + + +parse::usage = +"\ +parse[mode][boxes] \ +returns given boxes with certain subboxes wrapped with syntaxBox identifying \ +their syntactic role, governed by given parsing mode." + + +posExprPosition::usage = +"\ +posExprPosition[posExpr] \ +returns position encoded in given \"position expression\" posExpr.\ + +posExprPosition[posExpr, i] \ +returns position encoded in posExpr with last i elements droped." + + +normalizeAnnotationTypes::usage = +"\ +normalizeAnnotationTypes[types] \ +returns List of strings with names of syntax types used by Mathematica from \ +given List of annotation types used internally by this package." + + +syntaxStyleBox::usage = +"\ +syntaxStyleBox[boxes, annotationTypes] \ +returns StyleBox containing given boxes with proper style for given List of \ +annotationTypes." + + +$stringBoxToTypes::usage = +"\ +$stringBoxToTypes \ +is List of default rules converting string boxes to annotation types." + + +(* ::Subsection:: *) +(*operator groups*) + + +$patternOperators = "=" | "^=" | "->" | "\[Rule]" + +$patternDelayedOperators = ":=" | "^:=" | ":>" | "\[RuleDelayed]" + +$assignmentOperators = "=" | ":=" | "^=" | "^:=" + +$ruleOperators = "->" | "\[Rule]" | ":>" | "\[RuleDelayed]" + + +(* ::Subsection:: *) +(*symbolNameQ*) + + +symbolNameQ["Null"] = True + +symbolNameQ[str_String] := + And[ + StringFreeQ[str, WhitespaceCharacter], + MatchQ[ + Quiet @ MakeExpression[str, StandardForm], + HoldComplete[Except[Null | Symbol[___], _Symbol]] + ] + ] + +symbolNameQ[_] = False + + +(* ::Subsection:: *) +(*whitespaceQ*) + + +whitespaceQ = + StringMatchQ[ + StringJoin[##], + (WhitespaceCharacter | "\[IndentingNewLine]")... + ]& + + +(* ::Subsection:: *) +(*undefinedSymbolQ*) + + +SetAttributes[undefinedSymbolQ, HoldFirst] + + +undefinedSymbolQ[ + s : _String | _Symbol /; Quiet[Context[s], Context::notfound] === "System`" +] = False + +undefinedSymbolQ[name_String /; StringFreeQ[name, WhitespaceCharacter]] := + With[{heldSym = Quiet @ MakeExpression[name, StandardForm]}, + undefinedSymbolQ @@ heldSym /; + MatchQ[heldSym, HoldComplete[Except[Null | Symbol[___], _Symbol]]] + ] + +undefinedSymbolQ[sym_Symbol] := + And @@ (#@sym === {}& /@ { + OwnValues, SubValues, UpValues, DownValues, NValues, FormatValues, + DefaultValues, Messages + }) + +undefinedSymbolQ[_] = False + + +(* ::Subsection:: *) +(*extractSymbolName*) + + +extractSymbolName[str_String] := + Select[StringSplit[str, "_"], symbolNameQ, 1] + + +(* ::Subsection:: *) +(*extractLocalVariableNames*) + + +extractLocalVariableNames[_][Repeated[_, {0, 1}]] = {} + +extractLocalVariableNames[type_][argsBoxes__] := + extractLocalVariableNames[type] /@ {argsBoxes} // Flatten + +extractLocalVariableNames["Table" | "Plot" | "Integrate"][ + RowBox[{"{", RowBox[{name_String?symbolNameQ, ",", ___}], "}"}] +] := + {name} + +extractLocalVariableNames["Solve" | "Integrate"][name_String?symbolNameQ] := + {name} + +extractLocalVariableNames["Solve"][ + RowBox[{"{", RowBox[argBoxes : {__}], "}"}] +] := + Cases[argBoxes, _String?symbolNameQ] // Flatten + +extractLocalVariableNames["Solve"][ + RowBox[{"{", name_String?symbolNameQ, "}"}] +] := + {name} + +extractLocalVariableNames["Limit"][ + RowBox[{name_String?symbolNameQ, "\[Rule]" | "->", __}] +] := + {name} + +extractLocalVariableNames["Manipulate"][ + RowBox[{"{", + RowBox[{ + name_String?symbolNameQ | + RowBox[{"{", + RowBox[{name_String?symbolNameQ, ",", ___}], + "}"}], + ",", + ___ + }], + "}"}] +] := + {name} + +extractLocalVariableNames["LowerBound"][ + RowBox[{name_String?symbolNameQ, $assignmentOperators, ___}] +] := + {name} + +extractLocalVariableNames["LowerBound"][boxes_] := + Cases[boxes, _?symbolNameQ, {2}] // Flatten + +extractLocalVariableNames["IntegrateDifferentialD"][ + RowBox[{"\[DifferentialD]", name_String?symbolNameQ}] +] := + {name} + +(* \[Function] extracts symbol names and non-named blank heads as local + variables from everything except: + * func[...] with func having any kind of local variable specification, + * LHS of \[Function] constructs, + * RHS of assignments. + + Function accepts symbol and non-named blank head as single local variable, + and from a List of potential variables extacts same things as \[Function]. + + Scoping functions extract symbol names and non-named blank heads as local + variables from a List with same exceptions as \[Function]. From LHS of + assignment, pattern names are also extracted, so we use two "inner modes" + of extraction for scoping: + * {"Scoping", "List"} for boxes inside List, but not on LHS of assignment, + * {"Scoping", "LHS"} for boxes on LHS of assignment inside List. + + Assignment functions extract pattern names, as local variables, from + everything except RHS of inner assignments. + + Non assignmet patterns, i.e. rules and infix Condition, extract pattern + names as local variables from everything with same exceptions as + \[Function]. *) + +extractLocalVariableNames["Scoping"][RowBox[{"{", argBoxes_, "}"}]] := + extractLocalVariableNames[{"Scoping", "List"}][argBoxes] + +extractLocalVariableNames["Function"][RowBox[{"{", argBoxes_, "}"}]] := + extractLocalVariableNames["\[Function]"][argBoxes] + +extractLocalVariableNames[ + type : {"Scoping", _} | "NonAssigPatt" | "\[Function]" +][ + RowBox[{ + funcName_String /; + MemberQ[extendedSyntaxInformation[funcName], + "LocalVariables" -> _ + ], + "[", + __ + }] +] := + extractLocalVariableNames[type][funcName] + +extractLocalVariableNames[ + type : {"Scoping", _} | "NonAssigPatt" | "\[Function]" +][ + RowBox[{__, "\[Function]", rhs__}] +] := + extractLocalVariableNames[type] /@ {rhs} // Flatten + +extractLocalVariableNames[{"Scoping", _}][ + RowBox[{lhs__, $assignmentOperators, __}] +] := + extractLocalVariableNames[{"Scoping", "LHS"}] /@ {lhs} // Flatten + +extractLocalVariableNames[ + type : "NonAssigPatt" | "Assignment" | "\[Function]" +][ + RowBox[{lhs__, $assignmentOperators, __}] +] := + extractLocalVariableNames[type] /@ {lhs} // Flatten + +extractLocalVariableNames[type : "NonAssigPatt" | "Assignment"][ + RowBox[{name_String, ":", rest__}] +] := + { + extractSymbolName[name], + extractLocalVariableNames[type] /@ {rest} + } // Flatten + +extractLocalVariableNames[type : {"Scoping", "List"} | "\[Function]"][ + RowBox[{_String, ":", rest__}] +] := + extractLocalVariableNames[type] /@ {rest} // Flatten + +extractLocalVariableNames[{"Scoping", "List"} | "Function" | "\[Function]"][ + name_String /; StringMatchQ[name, "_"... ~~ Except["_"]...] +] := + extractSymbolName[name] + +extractLocalVariableNames[{"Scoping", "LHS"}][name_String] := + extractSymbolName[name] + +extractLocalVariableNames["NonAssigPatt" | "Assignment"][ + (name_String /; StringMatchQ[name, Except["_"].. ~~ "_" ~~ ___]) +] := + extractSymbolName[name] + +extractLocalVariableNames[ + type : {"Scoping", _} | "NonAssigPatt" | "Assignment" | "\[Function]" +][boxes:(_[__])] := + extractLocalVariableNames[type] /@ List @@ boxes // Flatten + + +(* ::Subsection:: *) +(*extractArgs*) + + +extractArgs[syntaxBox[arg_, _], spec_] := extractArgs[arg, spec] + +extractArgs[boxes_, 0] := {boxes} /. syntaxBox[var_, _] :> var + +extractArgs[arg_String, {min_, max_} /; min <= 1 <= max] := {arg} + +extractArgs[RowBox[argsBoxes:{___}], {min_Integer, max:_Integer|Infinity}] := + Module[{args, $previousComma = True}, + args = argsBoxes /. syntaxBox[var_, _] :> var; + (* It's possible that RowBox contains adjacent comma strings, or comma + string as first or last element. Such boxes are parsed, the same as + if there was empty or whitespace only String between commas, to + expression with Null argument betwenn commas, or before/after comma + if it is first/last in row. To take this into account in arguments + counting, we put empty strings in relevant places. *) + args = + Replace[args, { + "," :> + If[$previousComma, + $previousComma = True; + "" + (* else *), + $previousComma = True; + Unevaluated@Sequence[] + ], + x_ :> ($previousComma = False; x) + }, {1}]; + If[$previousComma, + AppendTo[args, ""] + ]; + Take[args, {Max[1, min], Min[Length[args], max]}] + ] + +extractArgs[argsBoxes_, {i_}] := extractArgs[argsBoxes, {i, i}] + +extractArgs[_, {_, _}] = {} + + +(* ::Subsection:: *) +(*extendedSyntaxInformation*) + + +extendedSyntaxInformation[symName : "Block" | "Module" | "With"] := + Append[ + SyntaxInformation[Symbol[symName]], + "LocalVariables" -> {"Scoping", {1}} + ] + +extendedSyntaxInformation[symName : "Function" | "\[Function]"] := + Append[ + SyntaxInformation[Function], + "LocalVariables" -> {symName, {1}} + ] + +extendedSyntaxInformation["\[Sum]" | "\[Product]"] := + {"LocalVariables" -> {"LowerBound", 0}} + +extendedSyntaxInformation["\[Integral]"] := + {"LocalVariables" -> {"IntegrateDifferentialD", {2, \[Infinity]}}} + +extendedSyntaxInformation[$ruleOperators | "/;"] := + {"LocalVariables" -> {"NonAssigPatt", {1}}} + +extendedSyntaxInformation[$assignmentOperators] := + {"LocalVariables" -> {"Assignment", {1}}} + +extendedSyntaxInformation["/:", $assignmentOperators] := + {"LocalVariables" -> {"Assignment", {1, 2}}} + +extendedSyntaxInformation[symName_String] := + SyntaxInformation[Quiet[Symbol[symName], Symbol::symname]] + + +(* ::Subsection:: *) +(*$inFunction*) + + +$inFunction = False + + +(* ::Subsection:: *) +(*$directlyInScopingRHS*) + + +$directlyInScopingRHS = False + + +(* ::Subsection:: *) +(*patternNameTypes*) + + +patternNameTypes[_] = {} + + +(* ::Subsection:: *) +(*stringBoxTypes*) + + +stringBoxTypes[str_] := + With[{split = StringSplit[str, "_", 2]}, + patternNameTypes @ First[split] /; MatchQ[split, {Except[""], _}] + ] + +stringBoxTypes[_] = {} + + +(* ::Subsection:: *) +(*deafultStringBoxTypes*) + + +deafultStringBoxTypes[_] = {} + + +(* ::Subsection:: *) +(*modifyTypes*) + + +modifyTypes[registers_List, type_, symNames_List] := + Scan[modifyTypes[#, type, symNames]&, registers] + +modifyTypes[stringBoxTypes, type_, symNames_List] := + Scan[ + ( + AppendTo[stringBoxTypes[#], type]; + + stringBoxTypes["_" <> #] = + stringBoxTypes["__" <> #] = + stringBoxTypes["___" <> #] = + Append[stringBoxTypes["_" <> #], type]; + )&, + symNames + ] + +modifyTypes[patternNameTypes, type_, symNames_List] := + Scan[AppendTo[patternNameTypes[#], type]&, symNames] + + +(* ::Subsection:: *) +(*withModifiedTypes*) + + +SetAttributes[withModifiedTypes, HoldRest] + +withModifiedTypes[ + heads_List:{patternNameTypes, stringBoxTypes}, + {type_, symNames_List}, + body_ +] := + Internal`InheritedBlock[heads, + modifyTypes[heads, type, symNames]; + + body + ] + + +(* ::Subsection:: *) +(*withLocalVariables*) + + +SetAttributes[withLocalVariables, {HoldAll, SequenceHold}] + + +withLocalVariables /: Verbatim[SetDelayed][ + lhs_, + withLocalVariables[{funcName_, localVarsSym_Symbol, argumentBoxes_}, body_] +] := ( + lhs := + With[ + { + localVariables = + "LocalVariables" /. extendedSyntaxInformation[funcName] + } + , + With[ + { + localVarsSym = + DeleteDuplicates[ + extractLocalVariableNames[localVariables[[1]]] @@ + extractArgs[argumentBoxes, localVariables[[2]]] + ] + } + , + body + ] /; Length[localVariables] >= 2 + ] +) + + +(* ::Subsection:: *) +(*parseScopingVars*) + + +(* List in variable specificaition can contain comma separated sequence of + variable specifications. *) +parseScopingVars[mode_, funcName_, localVars_][RowBox[boxes:{_, ",", ___}]] := + RowBox[parseScopingVars[mode, funcName, localVars] /@ boxes] + +(* List in variable specificaition can contain an assignment. *) +parseScopingVars[mode_, funcName_, localVars_][ + RowBox[{lhs__, op:$assignmentOperators, rhs___}] +] := + RowBox@Join[ + withModifiedTypes[{{"Scoping", funcName}, localVars}, + parse[mode] /@ {lhs, op} + ] + , + Block[{$directlyInScopingRHS = True}, + withModifiedTypes[{{"Scoping", funcName, "RHS"}, localVars}, + parse[mode] /@ {rhs} + ] + ] + ] + +(* Anything else is parsed with annotated local variables of this scope. *) +parseScopingVars[mode_, funcName_, localVars_][boxes___] := + withModifiedTypes[{{"Scoping", funcName}, localVars}, + Sequence @@ (parse[mode] /@ {boxes}) + ] + + + +(* ::Subsection:: *) +(*parse*) + + +parse[_][str_String] := + Module[{types = stringBoxTypes[str]}, + If[types =!= {}, + If[!$directlyInScopingRHS, + PrependTo[types, "NotDirectlyInScopingRHS"] + ]; + + syntaxBox[str, types] + (* else *), + str + ] + ] + +parse[_][boxes_?AtomQ] := boxes + +parse[mode_][RowBox[{name_String, ":", rest__}]] := + RowBox@Join[ + { + Module[ + { + types = + Join[ + deafultStringBoxTypes[name], + patternNameTypes @@ extractSymbolName[name] + ] + }, + If[types =!= {}, + If[!$directlyInScopingRHS, + PrependTo[types, "NotDirectlyInScopingRHS"] + ]; + + syntaxBox[name, types] + (* else *), + name + ] + ], + ":" + }, + parse[mode] /@ {rest} + ] + +parse[_][RowBox[{"::"}]] = RowBox[{"::"}] + +parse[mode_][ + RowBox[{sym___, "::", tag___, "::", lang___, "::", excess___}] +] := + RowBox@Join[ + parse[mode] /@ {sym}, + {If[{sym} === {}, syntaxBox["::", {"SyntaxError"}], "::"]}, + syntaxBox[#, {"String"}] & /@ {tag}, + {"::"}, + syntaxBox[#, {"String"}] & /@ {lang}, + {syntaxBox["::", {"ExcessArgument"}]}, + syntaxBox[#, {"ExcessArgument"}] & /@ {excess} + ] + +parse[mode_][RowBox[{sym___, "::", tag___, "::", lang___}]] := + RowBox@Join[ + parse[mode] /@ {sym}, + {If[{sym} === {}, syntaxBox["::", {"SyntaxError"}], "::"]}, + syntaxBox[#, {"String"}] & /@ {tag}, + {If[{lang} === {}, syntaxBox["::", {"SyntaxError"}], "::"]}, + syntaxBox[#, {"String"}] & /@ {lang} + ] + +parse[mode_][RowBox[{sym___, "::", tag___}]] := + RowBox@Join[ + parse[mode] /@ {sym}, + { + If[{sym} === {} || {tag} === {}, + syntaxBox["::", {"SyntaxError"}] + (* else *), + "::" + ] + }, + syntaxBox[#, {"String"}] & /@ {tag} + ] + +parse[mode:Except["AssignmentLHS"]][ + RowBox[{ + funcName : "With" | "Module" | "Block", "[", + args:RowBox[{ + RowBox[{"{", varSpecBoxes_ | PatternSequence[], "}"}], + ",", + restArgs___ + }], + "]" + }] +] := + withLocalVariables[{funcName, localVars, args}, + RowBox[{ + parse[mode][funcName] + , + "[" + , + (* Being inside inner scoping construct overrides effects of being + on RHS of assignment in variable specification of outer scoping + construct. *) + Block[{$directlyInScopingRHS = False}, + RowBox[{ + RowBox[{"{", + (* Symbols on RHS of assignments in scoping construct + variable specification are not local variables of + this scoping construct, switch to special + sub-parser that takes this into account. *) + parseScopingVars[mode, funcName, localVars][ + varSpecBoxes + ], + "}"}], + ",", + withModifiedTypes[{{"Scoping", funcName}, localVars}, + Sequence @@ parse[mode][{restArgs}] + ] + }] + ] + , + "]" + }] + ] + +parse[mode_][ + RowBox[boxes : ( + {"Function", "[", Except[RowBox[{_, ",", ___}]], "]"} | + {_, "&"} + )] +] := + Block[{$inFunction = True}, + RowBox[parse[mode] /@ boxes] + ] + +parse[mode:Except["AssignmentLHS"]][ + RowBox[{ + funcName:Except["With" | "Module" | "Block", _String], "[", args_, "]" + }] +] := + withLocalVariables[{funcName, localVars, args}, + RowBox[{ + parse[mode][funcName], + "[", + (* Being inside function having local variable specification + overrides effects of being on RHS of assignment in variable + specification of outer scoping construct. *) + Block[{$directlyInScopingRHS = False}, + withModifiedTypes[ + { + {Replace[funcName, { + "Function" -> "PatternVariable", + _ -> "FunctionLocalVariable" + }], funcName}, + localVars + } + , + parse[mode][args] + ] + ], + "]" + }] + ] + +parse[mode:Except["AssignmentLHS"]][RowBox[{lhs_, "\[Function]", rhs_}]] := + withLocalVariables[{"\[Function]", localVars, RowBox[{lhs, ",", rhs}]}, + RowBox[{ + (* Being inside LHS of \[Function] overrides effects of being on + RHS of assignment in variable specification of outer scoping + construct. *) + Block[{$directlyInScopingRHS = False}, + withModifiedTypes[ + {{"PatternVariable", "\[Function]"}, localVars}, + parse[mode][lhs] + ] + ], + parse[mode]["\[Function]"], + withModifiedTypes[{{"PatternVariable", "\[Function]"}, localVars}, + parse[mode][rhs] + ] + }] + ] + +parse[mode:Except["AssignmentLHS"]][ + RowBox[boxes:( + {UnderoverscriptBox[funcName:"\[Sum]" | "\[Product]", args_, _], _} | + { + SubsuperscriptBox[funcName : "\[Integral]", _, _] | + funcName : "\[Integral]" + , + args_ + } + )] +] := + withLocalVariables[{funcName, localVars, args}, + (* Being inside function having local variable specification overrides + effects of being on RHS of assignment in variable specification + of outer scoping construct. *) + Block[{$directlyInScopingRHS = False}, + withModifiedTypes[{{"FunctionLocalVariable", funcName}, localVars}, + RowBox[parse[mode] /@ boxes] + ] + ] + ] + +parse[mode:Except["AssignmentLHS"]][ + RowBox[{lhs_, funcName:$ruleOperators, rhs_}] +] := + withLocalVariables[{funcName, localVars, RowBox[{lhs, ",", rhs}]}, + RowBox[{ + withModifiedTypes[{patternNameTypes}, + {{"PatternVariable", funcName, "LHS"}, localVars}, + parse[mode][lhs] + ] + , + parse[mode][funcName] + , + If[MatchQ[funcName, $patternDelayedOperators], + (* Patterns, on right hand side of delayed rule, with same + name as one of patterns on left hand side, are marked as + local scope conflict not as pattern variables. *) + withModifiedTypes[{patternNameTypes}, + {{"LocalScopeConflict", funcName, "RHS"}, localVars}, + withModifiedTypes[{stringBoxTypes}, + {{"PatternVariable", funcName, "RHS"}, localVars}, + parse[mode][rhs] + ] + ] + (* else *), + parse[mode][rhs] + ] + }] + ] + +parse[mode_][RowBox[boxes : {patt_, "/;", test_}]] := + withLocalVariables[{"/;", localVars, RowBox[{patt, ",", test}]}, + withModifiedTypes[{{"PatternVariable", "/;"}, localVars}, + RowBox[parse[mode] /@ boxes] + ] + ] + +parse[mode_][ + boxes : RowBox[{ + PatternSequence[tag_, tagSep:"/:"] | PatternSequence[], + lhs_, + funcName:$assignmentOperators, + rhs_ + }] +] := + withLocalVariables[ + { + Sequence[tagSep, funcName], + localVars, + Replace[boxes, "/:" | $assignmentOperators -> ",", {2}] + } + , + RowBox[{ + withModifiedTypes[{patternNameTypes}, + {{"PatternVariable", funcName, "LHS"}, localVars} + , + (* Constructs inside LHS of assignments don't change syntax + roles with exception of other assignments, so we switch to + "AssignmentLHS" parsing mode and set $inFunction to + False. *) + Sequence @@ Block[{$inFunction = False}, + parse["AssignmentLHS"] /@ {tag, tagSep, lhs} + ] + ] + , + parse[mode][funcName] + , + (* Being on RHS of inner assignment overrides effects of being + on RHS of assignment in variable specification of outer scoping + construct. *) + Block[{$directlyInScopingRHS = False}, + (* Being on RHS of inner assignment overrides effects of being + on LHS of outer assignment, so switch to "Main" parsing + mode. *) + If[MatchQ[funcName, $patternDelayedOperators], + (* Patterns, on right hand side of delayed assignment, + with same name as one of patterns on left hand side, + are marked as local scope conflict not as pattern + variables. *) + withModifiedTypes[{patternNameTypes}, + {{"LocalScopeConflict", funcName, "RHS"}, localVars}, + withModifiedTypes[{stringBoxTypes}, + {{"PatternVariable", funcName, "RHS"}, localVars}, + parse["Main"][rhs] + ] + ] + (* else *), + parse["Main"][rhs] + ] + ] + }] + ] + +parse[mode_][boxes_] := parse[mode] /@ boxes + + +(* ::Subsection:: *) +(*posExprPosition*) + + +posExprPosition[expr_] := posExprPosition[expr, 0] + +posExprPosition[pos_List, i_] := Drop[pos, -i] + +posExprPosition[head_[___], i_] := posExprPosition[head, i + 1] + + +(* ::Subsection:: *) +(*normalizeAnnotationTypes*) + + +(* Local variables of scoping construct that are on RHS of assignment in + variable specification, but not directly, i.e. they are inside some other + function with local variables, are treated as local variables of scoping + construct. *) +normalizeAnnotationTypes[{"NotDirectlyInScopingRHS", types___}] := + normalizeAnnotationTypes @ + Replace[{types}, + {"Scoping", funcName_, "RHS"} :> {"Scoping", funcName}, + {1} + ] + +normalizeAnnotationTypes[{"UndefinedSymbol", types___}] := + Append[normalizeAnnotationTypes[{types}], "UndefinedSymbol"] + +normalizeAnnotationTypes[types_List] := + Module[ + { + newTypes = types, reversedTypes = Reverse[types], + scopingPos, pattVarNonRuleLhsPos, lastPattConfPos, locScopeConfPos, + locVarPos, funcLocVarPos + }, + (* Local scope conflict arises when we have: + * Scoping inside Scoping, + * Scoping, any non-\[Function] and non-/; PatternVariable, or + LocalScopeConflict from using pattern on RHS of delayed rule or + assignment, inside PatternVariable that is not from \[Function] + nor LHS of a rule. + LocalScopeConflict type appears just before first type involved in + conflict. *) + scopingPos = + Position[newTypes, {"Scoping", __}, {1}, 2, Heads -> False]; + pattVarNonRuleLhsPos = + Position[newTypes, + Except[ + {"PatternVariable", $ruleOperators, "LHS"}, + {"PatternVariable", Except["\[Function]" | "/;"], ___} + ], + {1}, 2, Heads -> False + ]; + lastPattConfPos = + Length[newTypes] + 1 - + Position[reversedTypes, + { + "PatternVariable" | "LocalScopeConflict", + Except["\[Function]" | "/;"], + ___ + }, + {1}, 1, Heads -> False + ]; + locScopeConfPos = + Which[ + (* , + first non-Rule-LHS non-\[Function] non-/; PatternVariable, + ..., + first Scoping, + ... *) + pattVarNonRuleLhsPos =!= {} && scopingPos =!= {} && + pattVarNonRuleLhsPos[[1, 1]] < scopingPos[[1, 1]] + , + pattVarNonRuleLhsPos[[1, 1]] + , + (* , first Scoping, ..., second Scoping, .... *) + Length[scopingPos] >= 2, + scopingPos[[1, 1]] + , + (* , + first non-Rule-LHS non-\[Function] non-/; PatternVariable, + ..., + last Scoping or any PatternVariable, + ... *) + pattVarNonRuleLhsPos =!= {} && + pattVarNonRuleLhsPos[[1, 1]] < lastPattConfPos[[1, 1]] + , + pattVarNonRuleLhsPos[[1, 1]] + , + True, + {} + ]; + If[locScopeConfPos =!= {}, + (* LocalScopeConflict imposes non-iatlic style, even if giving + italic PatternVariable is present, when last type involved in + conflict is not of PatternVariable type. Emulate this behavior + by removing all PatternVariable after LocalScopeConflict. *) + newTypes = + If["PatternVariable" =!= + First @ Cases[reversedTypes, + { + type : + "Scoping" | + "PatternVariable" | + "LocalScopeConflict", + __ + } :> type + , + {1}, + 1 + ] + (* then *), + Join[ + Take[newTypes, locScopeConfPos - 1], + {"LocalScopeConflict"}, + DeleteCases[ + Drop[newTypes, locScopeConfPos - 1], + {"PatternVariable", __} + ] + ] + (* else *), + Insert[newTypes, "LocalScopeConflict", locScopeConfPos] + ]; + ]; + + (* LocalVariable (With, Module) removes all outer Block types. *) + locVarPos = + Position[Reverse[newTypes], + {"Scoping", "With" | "Module", ___}, + {1}, + 1, + Heads -> False + ]; + If[locVarPos =!= {}, + locVarPos = -locVarPos[[1, 1]]; + newTypes = + Join[ + DeleteCases[ + Drop[newTypes, locVarPos], + {"Scoping", "Block", ___} + ], + Take[newTypes, locVarPos] + ] + ]; + + (* FunctionLocalVariable, coming from anything else than Block, + removes LocalVariable immediately before it, and is always moved + to the end. *) + funcLocVarPos = + Position[newTypes, + {"FunctionLocalVariable", __}, + {1}, + Heads -> False + ]; + locVarPos = + Select[ + Replace[funcLocVarPos, {{1}, ___} :> Rest[funcLocVarPos]] - 1, + MatchQ[newTypes[[First[#]]], + {"Scoping", "With" | "Module", ___} + ]& + ]; + If[funcLocVarPos =!= {}, + newTypes = + Append[ + Delete[newTypes, Join[funcLocVarPos, locVarPos]], + "FunctionLocalVariable" + ] + ]; + + DeleteDuplicates @ Replace[ + newTypes, { + {"Scoping", _, "RHS"} -> Sequence[], + {"Scoping", "Block", ___} -> "FunctionLocalVariable", + {"Scoping", ___} -> "LocalVariable", + {type_, ___} :> type + }, + {1} + ] + ] + + +(* ::Subsection:: *) +(*syntaxStyleBox*) + + +syntaxStyleBox[boxes_, annotationTypes_] := + StyleBox[ + boxes, + CurrentValue[{AutoStyleOptions, # <> "Style"}]& /@ annotationTypes + ] + + +(* ::Subsection:: *) +(*$stringBoxToTypes*) + + +$stringBoxToTypes = { + str_ /; $inFunction && StringMatchQ[str, "#*"] :> + {{"PatternVariable", "Function"}} + , + str_ /; StringMatchQ[str, "\"*\""] -> {"String"} + , + _?undefinedSymbolQ -> {"UndefinedSymbol"} +} + + +(* ::Subsection:: *) +(*AnnotateSyntax*) + + +Options[AnnotateSyntax] = { + "Annotation" -> Automatic, + "StringBoxToTypes" -> Automatic, + "AnnotateComments" -> True +} + + +AnnotateSyntax[boxes_, OptionsPattern[]] := + Module[ + { + annotation = + Replace[OptionValue["Annotation"], Automatic -> syntaxStyleBox] + , + stringBoxToTypes = + Replace[ + OptionValue["StringBoxToTypes"], + Automatic -> $stringBoxToTypes, + {0, 1} + ] // Flatten + , + commentPlaceholder, boxesCommRepl, commPos, boxesComm, ignoredPos, + boxesClean, boxesCleanParsed, syntaxPosClean, syntaxPos + }, + + boxesCommRepl = + boxes /. RowBox[{"(*", ___, "*)"}] -> commentPlaceholder; + commPos = + Position[boxesCommRepl, commentPlaceholder, {-1}, Heads -> False]; + boxesComm = + If[TrueQ @ OptionValue["AnnotateComments"], + MapAt[annotation[#, {"Comment"}]&, boxes, commPos] + (* else *), + boxes + ]; + ignoredPos = + Join[ + Position[boxesCommRepl, + _String?whitespaceQ, + {-1}, + Heads -> False + ], + commPos + ]; + boxesClean = Delete[boxesCommRepl, ignoredPos]; + If[{boxesClean} === {}, Return[boxesComm, Module]]; + + Internal`InheritedBlock[{deafultStringBoxTypes, stringBoxTypes}, + Function[{lhs, rhs}, + deafultStringBoxTypes[lhs] := rhs; + stringBoxTypes[lhs] := rhs + , + HoldAll + ] @@@ + stringBoxToTypes; + boxesCleanParsed = parse["Main"][boxesClean] + ]; + + syntaxPosClean = + Position[boxesCleanParsed, _syntaxBox, Heads -> False]; + syntaxPos = + Extract[ + Delete[ + MapIndexed[#2&, boxes, {-1}, Heads -> True], + ignoredPos + ], + syntaxPosClean, + posExprPosition + ]; + ReplacePart[boxesComm, + MapThread[ + With[{normalizedTypes = normalizeAnnotationTypes @ Last[#2]}, + If[normalizedTypes === {}, + Unevaluated @ Sequence[] + (* else *), + {#1} -> annotation[#3, normalizedTypes] + ] + ] &, + { + syntaxPos, + Extract[boxesCleanParsed, syntaxPosClean], + Extract[boxes, syntaxPos] + } + ] + ] + ] + + +(* ::Section:: *) +(*Package Epilogue*) + + +End[] + + +Protect@Evaluate@Names[Context[] ~~ Except["$"] ~~ Except["`"]...] + + +EndPackage[] + +Print["Everithing was OK"]; diff --git a/test/builtin/test_assignment.py b/test/builtin/test_assignment.py index cb4e42d61..51f06ee67 100644 --- a/test/builtin/test_assignment.py +++ b/test/builtin/test_assignment.py @@ -5,13 +5,6 @@ from test.helper import check_evaluation, session from mathics_scanner.errors import IncompleteSyntaxError -DEBUGASSIGN = int(os.environ.get("DEBUGSET", "0")) == 1 - -if DEBUGASSIGN: - skip_or_fail = pytest.mark.xfail -else: - skip_or_fail = pytest.mark.skip - str_test_set_with_oneidentity = """ SetAttributes[SUNIndex, {OneIdentity}]; @@ -215,28 +208,6 @@ def test_setdelayed_oneidentity(): None, None, ), - ], -) -def test_set_and_clear(str_expr, str_expected, msg): - """ - Test calls to Set, Clear and ClearAll. If - str_expr is None, the session is reset, - in a way that the next test run over a fresh - environment. - """ - check_evaluation( - str_expr, - str_expected, - to_string_expr=True, - to_string_expected=True, - hold_expected=True, - failure_message=msg, - ) - - -@pytest.mark.parametrize( - ("str_expr", "str_expected", "msg"), - [ (None, None, None), (r"a=b; a=4; {a, b}", "{4, b}", None), (None, None, None), @@ -276,7 +247,7 @@ def test_set_and_clear(str_expr, str_expected, msg): (None, None, None), ( ( - "A[x_]:=B[x];B[x_]:=F[x_];F[x_]:=G[x];" + "A[x_]:=B[x];B[x_]:=F[x];F[x_]:=G[x];" "H[A[y_]]:=Q[y]; ClearAll[F];" "{H[A[5]],H[B[5]],H[F[5]],H[G[5]]}" ), @@ -289,10 +260,39 @@ def test_set_and_clear(str_expr, str_expected, msg): "{F[2.], 4.}", "Assign N rule", ), + (None, None, None), + # This test is inspirated in CellsToTeX + ("SetAttributes[testHoldAll, HoldAll]", "Null", None), + ( + ( + "addF[sym_Symbol] := (" + " functionCall:sym[___] := " + " holdallfunc[functionCall]" + " )" + ), + "Null", + None, + ), + ("addF[Q]", "Null", None), + ("Q[1]", "holdallfunc[Q[1]]", None), + ( + """ + ClearAll[F]; + F[k_] := 1 /; (k == 1); + F[k_] := 2 /; (k > 1); + F[k_] := Q[3] ; + F[k_] := M[3] ; + {F[0],F[1],F[2]} + """, + "{M[3], 1, 2}", + ( + "sucesive set* with the same LHS but different RHS overwrites sucesively," + "except if the RHS are composed by conditions." + ), + ), ], ) -@skip_or_fail -def test_set_and_clear_to_fix(str_expr, str_expected, msg): +def test_set_and_clear(str_expr, str_expected, msg): """ Test calls to Set, Clear and ClearAll. If str_expr is None, the session is reset, diff --git a/test/builtin/test_patterns.py b/test/builtin/test_patterns.py index 9fb3d8a0f..507c331f9 100644 --- a/test/builtin/test_patterns.py +++ b/test/builtin/test_patterns.py @@ -26,3 +26,31 @@ def test_replace_all(): ), ): check_evaluation(str_expr, str_expected, message) + + +def test_rule_repl_cond(): + for str_expr, str_expected, message in ( + # For Rules, replacement is not evaluated + ( + "f[x]/.(f[u_]->u^2/; u>3/; u>2)", + "x^2/; x>3/; x>2", + "conditions are not evaluated in Rule", + ), + ( + "f[4]/.(f[u_]->u^2/; u>3/; u>2)", + "16 /; 4 > 3 /; 4 > 2", + "still not evaluated, even if values are provided, due to the HoldAll attribute.", + ), + # However, for delayed rules, the behavior is different: + # Conditions defines if the rule is applied + # and do not appears in the result. + ("f[x]/.(f[u_]:>u^2/; u>3/; u>2)", "f[x]", "conditions are not evaluated"), + ("f[4]/.(f[u_]:>u^2/; u>3/; u>2)", "16", "both conditions are True"), + ( + "f[2.5]/.(f[u_]:>u^2/; u>3/; u>2)", + "f[2.5]", + "just the first condition is True", + ), + ("f[1.]/.(f[u_]:>u^2/; u>3/; u>2)", "f[1.]", "Both conditions are False"), + ): + check_evaluation(str_expr, str_expected, message) diff --git a/test/test_control.py b/test/test_control.py index 41ac0cf1e..45f359f46 100644 --- a/test/test_control.py +++ b/test/test_control.py @@ -69,7 +69,7 @@ def test_condition(): evaluate( """ (* Define a function that can "throw an exception": *) - + ClearAll[f]; f[x_] := ppp[x]/; x>0 """ )