protected ASTNode internalReduce(ASTNode term, final IExpressionContext context) {
    final ASTNode result = term.createCopy(true);

    final MutatingNodeVisitor visitor =
        new MutatingNodeVisitor(context) {

          @Override
          public void visit(ASTNode node, IExpressionContext context, IIterationContext it) {
            if (node.hasParent()) {
              if (node instanceof IdentifierNode && !node.hasLiteralValue(context)) {
                return;
              }

              final ASTNode reduced = node.evaluate(context);
              debugPrintln("REDUCE: " + node + " evaluates to " + reduced);
              if (reduced != null && reduced != node && reduced != unwrap(node)) {
                debugPrintln("REDUCE: Replacing " + node + " -> " + reduced);
                node.replaceWith(reduced);
                it.astMutated();
                it.stop();
              }
            }
          }
        };

    applyPostOrder(result, visitor);
    return result;
  }
  public ASTNode substituteCommonTerms(ASTNode tree, IExpressionContext context) {

    ASTNode copy = tree.createCopy(true);

    final Map<Integer, List<ASTNode>> result = new HashMap<>();

    final INodeVisitor visitor =
        new INodeVisitor() {

          @Override
          public boolean visit(ASTNode node, int currentDepth) {
            final Integer hash = node.hashCode();
            List<ASTNode> existing = result.get(hash);
            if (existing == null) {
              existing = new ArrayList<>();
              result.put(hash, existing);
            }
            existing.add(node);
            return true;
          }
        };
    tree.visitInOrder(visitor);

    outer:
    for (Map.Entry<Integer, List<ASTNode>> entry : result.entrySet()) {
      final int hash = entry.getKey();

      if (entry.getValue().size() <= 1) {
        continue;
      }

      for (ASTNode n : entry.getValue()) {
        if (n instanceof IdentifierNode) {
          continue outer;
        }
      }

      // create new variable
      final ASTNode value = entry.getValue().get(0).createCopy(true);
      final Identifier identifier = context.createIdentifier(value);

      final boolean substituted = replaceMatchingTermsWithVariable(context, copy, hash, identifier);

      if (substituted) {
        debugPrintln(
            "SUBSTITUTE: "
                + value
                + " => "
                + identifier
                + " ( "
                + entry.getValue().size()
                + " times )");
      } else {
        context.remove(identifier);
      }
    }
    return copy;
  }
  public ASTNode substituteIdentifiers(ASTNode input, IExpressionContext context) {

    ASTNode result = input.createCopy(true);

    final MutatingNodeVisitor visitor =
        new MutatingNodeVisitor(context) {

          @Override
          protected void visit(ASTNode node, IExpressionContext context, IIterationContext it) {
            if (node instanceof IdentifierNode) {
              Identifier id = ((IdentifierNode) node).getIdentifier();
              ASTNode value = context.tryLookup(id);
              if (value != null) {
                node.replaceWith(value);
                it.astMutated();
              }
            }
          }
        };

    applyInOrder(result, visitor);
    return result;
  }
  public ASTNode expand(ASTNode term, IExpressionContext context, boolean deleteExpandedVars) {
    final ASTNode result = term.createCopy(true);

    final Set<Identifier> expandedIdentifiers = new HashSet<>();

    boolean expanded = false;
    do {
      expanded = false;
      final MutatingNodeVisitor visitor =
          new MutatingNodeVisitor(context) {

            @Override
            public void visit(ASTNode node, IExpressionContext context, IIterationContext it) {
              final ASTNode unwrapped = unwrap(node);
              if (node.hasParent() && unwrapped instanceof IdentifierNode) {
                final ASTNode expanded = unwrapped.evaluate(context);
                if (expanded != null && expanded != unwrapped) {
                  debugPrintln("EXPAND: Expanding " + node + " -> " + expanded);
                  expandedIdentifiers.add(((IdentifierNode) unwrapped).getIdentifier());
                  node.replaceWith(expanded);
                  it.astMutated();
                }
              }
            }
          };

      expanded = applyPostOrder(result, visitor);
    } while (expanded);

    if (deleteExpandedVars) {
      for (Identifier id : expandedIdentifiers) {
        context.remove(id);
      }
    }

    return result;
  }
  protected ASTNode simplifyTerm(ASTNode term, final IExpressionContext context) {

    debugPrintln("Simplifying " + term.toString(true));

    final Comparator<ASTNode> comp =
        new Comparator<ASTNode>() {

          @Override
          public int compare(ASTNode o1, ASTNode o2) {
            if (o1.isLeafNode() && o2.isLeafNode()) {
              return o2.toString().compareTo(o1.toString());
            } else if (o1.isLeafNode()) {
              return 1;
            } else if (o2.isLeafNode()) {
              return -1;
            }
            return 0;
          }
        };

    ASTNode result = term.createCopy(true);

    result = reduce(result, context);

    int loopCounter = 0;

    boolean simplified = result.sortChildrenAscending(comp);
    do {
      simplified = false;

      // Assoziativgesetz
      // (a and b) and c = a and (b and c)
      // (a or b) or c = a or (b or c)
      simplified |= applyLawOfAssociativity(context, result);

      // Idempotenzgesetze:    => x and x = x
      //                       => x or  x = x
      simplified |= applyLawOfIdemPotency(context, result);

      // double negation:      => not( not a) = a
      simplified |= applyRuleOfDoubleNegation(context, result);

      // Neutralitätsgesetze  => a and 1 = a
      //                      => a or  0 = a
      simplified |= applyLawOfIdentity(context, result);

      // Extremalgesetze 	 => a and 0 =0
      //                      => a or  1 =1
      simplified |= applyLawOfExtrema(context, result);

      // Komplementärgesetze   => a and not a = 0
      //                       => a or  not a = 1
      simplified |= applyLawOfComplements(context, result);

      // Absorptionsgesetze 	 => a or (a and b) = a
      //                       => a and(a or  b) = a
      simplified |= applyLawOfAbsorption(context, result);

      // Distributionsgesetz
      // 	a and (b or  c) = (a and b) or  (a and c)
      //  a or  (b and c) = (a or  b) and (a or  c)
      simplified |= applyDistributiveLaw(context, result);

      // De Morgansche Gesetze  => not(a and b) = not a or  not b
      //                        => not(a or  b) = not a and not b
      simplified = applyLawOfDeMorgan(context, result);

      // De Morgansche Gesetze  => not a or  not b = not(a and b)
      //                        => not a and not b = not(a or  b)
      //			simplified |= applyInverseLawOfDeMorgan(context,result);
      loopCounter++;
    } while (simplified || loopCounter < 2);

    // get rid of all variables we eliminated
    context.retainOnly(gatherIdentifiers(result));
    return result;
  }