Macros.java

/*
 * Copyright (C) 1998-2018  Gerwin Klein <lsf@jflex.de>
 * SPDX-License-Identifier: BSD-3-Clause
 */

package jflex.core;

import static jflex.l10n.ErrorMessages.MACRO_CYCLE;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import jflex.base.Build;
import jflex.exceptions.MacroException;
import jflex.l10n.ErrorMessages;
import jflex.logging.Out;

/**
 * Symbol table and expander for macros.
 *
 * <p>Maps macros to their (expanded) definitions, detects cycles and unused macros.
 *
 * @author Gerwin Klein
 * @version JFlex 1.10.0-SNAPSHOT
 */
public final class Macros {

  /** Maps names of macros to their definition */
  private final Map<String, RegExp> macros;

  /** Maps names of macros to their "used" flag */
  private final Map<String, Boolean> used;

  /** Creates a new macro expander. */
  public Macros() {
    macros = new HashMap<>();
    used = new HashMap<>();
  }

  /**
   * Stores a new macro and its definition.
   *
   * @param name the name of the new macro
   * @param definition the definition of the new macro
   * @return {@code true}, iff the macro name has not been stored before.
   */
  public boolean insert(String name, RegExp definition) {

    if (Build.DEBUG)
      Out.debug(
          "inserting macro "
              + name
              + " with definition :"
              + Out.NL
              + definition); // $NON-NLS-1$ //$NON-NLS-2$

    used.put(name, Boolean.FALSE);
    return macros.put(name, definition) == null;
  }

  /**
   * Marks a macro as used.
   *
   * @return {@code true}, iff the macro name has been stored before.
   * @param name a {@link java.lang.String} object.
   */
  public boolean markUsed(String name) {
    return used.put(name, Boolean.TRUE) != null;
  }

  /**
   * Tests if a macro has been used.
   *
   * @return {@code true}, iff the macro has been used in a regular expression.
   * @param name a {@link java.lang.String} object.
   */
  public boolean isUsed(String name) {
    return used.get(name);
  }

  /**
   * Returns all unused macros.
   *
   * @return the macro names that have not been used.
   */
  public List<String> unused() {

    List<String> unUsed = new ArrayList<>();

    for (String name : used.keySet()) {
      Boolean isUsed = used.get(name);
      if (!isUsed) unUsed.add(name);
    }

    return unUsed;
  }

  /**
   * Fetches the definition of the macro with the specified name,
   *
   * <p>The definition will either be the same as stored (expand() not called), or an equivalent
   * one, that doesn't contain any macro usages (expand() called before).
   *
   * @param name the name of the macro
   * @return the definition of the macro, {@code null} if no macro with the specified name has been
   *     stored.
   * @see Macros#expand
   */
  public RegExp getDefinition(String name) {
    return macros.get(name);
  }

  /**
   * Expands all stored macros, so that getDefinition always returns a definition that doesn't
   * contain any macro usages.
   *
   * @throws MacroException if there is a cycle in the macro usage graph.
   */
  public void expand() throws MacroException {
    Set<String> keys = new HashSet<String>(macros.keySet());
    for (String name : keys) {
      if (isUsed(name)) {
        macros.replace(name, expandMacro(name, getDefinition(name)));
      }
    }
  }

  /**
   * Expands the specified macro by replacing each macro usage with the stored definition.
   *
   * @param name the name of the macro to expand (for detecting cycles)
   * @param definition the definition of the macro to expand
   * @return the expanded definition of the macro.
   * @throws MacroException when an error (such as a cyclic definition) occurs during expansion
   */
  @SuppressWarnings("unchecked")
  private RegExp expandMacro(String name, RegExp definition) throws MacroException {

    // Out.print("checking macro "+name);
    // Out.print("definition is "+definition);

    switch (definition.type) {
      case sym.BAR:
      case sym.CONCAT:
        RegExp2 binary = (RegExp2) definition;
        binary.r1 = expandMacro(name, binary.r1);
        binary.r2 = expandMacro(name, binary.r2);
        return definition;

      case sym.STAR:
      case sym.PLUS:
      case sym.QUESTION:
      case sym.BANG:
      case sym.TILDE:
        RegExp1 unary = (RegExp1) definition;
        unary.content = expandMacro(name, (RegExp) unary.content);
        return definition;

      case sym.MACROUSE:
        String usename = (String) ((RegExp1) definition).content;

        if (Objects.equals(name, usename))
          throw new MacroException(ErrorMessages.get(MACRO_CYCLE, name));

        RegExp usedef = getDefinition(usename);

        if (usedef == null) {
          throw new MacroException(
              ErrorMessages.get(ErrorMessages.MACRO_DEF_MISSING, usename, name));
        }

        markUsed(usename);

        return expandMacro(name, usedef);

      case sym.STRING:
      case sym.STRING_I:
      case sym.CHAR:
      case sym.CHAR_I:
      case sym.PRIMCLASS:
      case sym.PRECLASS:
      case sym.UNIPROPCCLASS:
        return definition;

      case sym.CCLASS:
      case sym.CCLASSNOT:
        RegExp1 cclass = (RegExp1) definition;
        List<RegExp> classes = new ArrayList<>();
        for (RegExp regexp : (List<RegExp>) cclass.content) {
          classes.add(expandMacro(name, regexp));
        }
        cclass.content = classes;
        return cclass;

      case sym.CCLASSOP:
        RegExp2 cclassOp = (RegExp2) ((RegExp1) definition).content;
        cclassOp.r1 = expandMacro(name, cclassOp.r1);
        cclassOp.r2 = expandMacro(name, cclassOp.r2);
        return definition;

      default:
        throw new MacroException(
            "unknown expression type " + definition.typeName() + " in macro expansion");
    }
  }
}