Merge pull request #220 from mbuesch/icepll-stdout

icepll: Add support for writing output data to stdout
diff --git a/icebox/icebox.py b/icebox/icebox.py
index a6852fc..c4b435c 100644
--- a/icebox/icebox.py
+++ b/icebox/icebox.py
@@ -16,7 +16,33 @@
 #
 
 import iceboxdb
-import re, sys
+import re, sys, functools
+
+
+if True:
+    # icebox uses lots of regular expressions.
+    # Supply cached versions of common re functions
+    # to avoid re-calculating regular expression results
+    # over and over again.
+    re_cache_sizes = 2**14
+
+    @functools.lru_cache(maxsize=re_cache_sizes)
+    def re_match_cached(*args):
+        return re.match(*args)
+
+    @functools.lru_cache(maxsize=re_cache_sizes)
+    def re_sub_cached(*args):
+        return re.sub(*args)
+
+    @functools.lru_cache(maxsize=re_cache_sizes)
+    def re_search_cached(*args):
+        return re.search(*args)
+else:
+    # Disable regular expression caching.
+    re_match_cached = re.match
+    re_sub_cached = re.sub
+    re_search_cached = re.search
+
 
 class iceconfig:
     def __init__(self):
@@ -36,6 +62,7 @@
         self.ram_data = dict()
         self.extra_bits = set()
         self.symbols = dict()
+        self.tile_has_net.cache_clear()
 
     def setup_empty_384(self):
         self.clear()
@@ -456,6 +483,7 @@
             return self.tile_has_net(x, y, entry[2]) and self.tile_has_net(x, y, entry[3])
         return True
 
+    @functools.lru_cache(maxsize=2**16)
     def tile_has_net(self, x, y, netname):
         if netname.startswith("logic_op_"):
             if netname.startswith("logic_op_bot_"):
@@ -576,11 +604,11 @@
             for net in self.follow_funcnet(x, y, 3) | self.follow_funcnet(x, y, 7):
                 if self.tile_pos(net[0], net[1]) == "x": funcnets.add(net)
 
-        match = re.match(r"lutff_(\d+)/out", netname)
+        match = re_match_cached(r"lutff_(\d+)/out", netname)
         if match:
             funcnets |= self.follow_funcnet(x, y, int(match.group(1)))
 
-        match = re.match(r"ram/RDATA_(\d+)", netname)
+        match = re_match_cached(r"ram/RDATA_(\d+)", netname)
         if match:
             if self.device == "1k":
                 funcnets |= self.follow_funcnet(x, y, int(match.group(1)) % 8)
@@ -594,7 +622,7 @@
         return funcnets
     
     def ultraplus_follow_corner(self, corner, direction, netname):
-        m = re.match("span4_(horz|vert)_([lrtb])_(\d+)$", netname)
+        m = re_match_cached("span4_(horz|vert)_([lrtb])_(\d+)$", netname)
         if not m:
             return None
         cur_edge = m.group(2)
@@ -676,12 +704,12 @@
                     if self.tile_pos(nx, ny) is not None:
                         neighbours.add((nx, ny, netname))
 
-        match = re.match(r"sp4_r_v_b_(\d+)", netname)
+        match = re_match_cached(r"sp4_r_v_b_(\d+)", netname)
         if match and ((0 < x < self.max_x-1) or (self.is_ultra() and (x < self.max_x))):
             neighbours.add((x+1, y, sp4v_normalize("sp4_v_b_" + match.group(1))))
         #print('\tafter r_v_b', neighbours)
 
-        match = re.match(r"sp4_v_[bt]_(\d+)", netname)
+        match = re_match_cached(r"sp4_v_[bt]_(\d+)", netname)
         if match and (1 < x < self.max_x or (self.is_ultra() and (x > 0))):
             n = sp4v_normalize(netname, "b")
             if n is not None:
@@ -689,7 +717,7 @@
                 neighbours.add((x-1, y, n))
         #print('\tafter v_[bt]', neighbours)
 
-        match = re.match(r"(logic|neigh)_op_(...)_(\d+)", netname)
+        match = re_match_cached(r"(logic|neigh)_op_(...)_(\d+)", netname)
         if match:
             if match.group(2) == "bot": nx, ny = (x,   y-1)
             if match.group(2) == "bnl": nx, ny = (x-1, y-1)
@@ -716,8 +744,8 @@
                         s = self.ultraplus_follow_corner(self.get_corner(s[0], s[1]), direction, n)
                         if s is None:
                             continue
-                    elif re.match("span4_(vert|horz)_[lrtb]_\d+$", n) and not self.is_ultra():
-                        m = re.match("span4_(vert|horz)_([lrtb])_\d+$", n)
+                    elif re_match_cached("span4_(vert|horz)_[lrtb]_\d+$", n) and not self.is_ultra():
+                        m = re_match_cached("span4_(vert|horz)_([lrtb])_\d+$", n)
 
                         vert_net = n.replace("_l_", "_t_").replace("_r_", "_b_").replace("_horz_", "_vert_")
                         horz_net = n.replace("_t_", "_l_").replace("_b_", "_r_").replace("_vert_", "_horz_")
@@ -976,7 +1004,7 @@
                 if line[0] == ".ipcon_tile":
                     self.ipcon_tiles[(int(line[1]), int(line[2]))] = current_data
                     continue
-                match = re.match(r".dsp(\d)_tile", line[0])
+                match = re_match_cached(r".dsp(\d)_tile", line[0])
                 if match:
                     self.dsp_tiles[int(match.group(1))][(int(line[1]), int(line[2]))] = current_data
                     continue
@@ -1069,7 +1097,7 @@
     valid_sp12_v_b = set(range(24))
 
 def sp4h_normalize(netname, edge=""):
-    m = re.match("sp4_h_([lr])_(\d+)$", netname)
+    m = re_match_cached("sp4_h_([lr])_(\d+)$", netname)
     assert m
     if not m: return None
     cur_edge = m.group(1)
@@ -1092,7 +1120,7 @@
 # "Normalization" of span4 (not just sp4) is needed during Ultra/UltraPlus
 # corner tracing
 def ultra_span4_horz_normalize(netname, edge=""):
-    m = re.match("span4_horz_([rl])_(\d+)$", netname)
+    m = re_match_cached("span4_horz_([rl])_(\d+)$", netname)
     assert m
     if not m: return None
     cur_edge = m.group(1)
@@ -1118,7 +1146,7 @@
     assert False
     
 def sp4v_normalize(netname, edge=""):
-    m = re.match("sp4_v_([bt])_(\d+)$", netname)
+    m = re_match_cached("sp4_v_([bt])_(\d+)$", netname)
     assert m
     if not m: return None
     cur_edge = m.group(1)
@@ -1140,7 +1168,7 @@
     return netname
 
 def sp12h_normalize(netname, edge=""):
-    m = re.match("sp12_h_([lr])_(\d+)$", netname)
+    m = re_match_cached("sp12_h_([lr])_(\d+)$", netname)
     assert m
     if not m: return None
     cur_edge = m.group(1)
@@ -1162,7 +1190,7 @@
     return netname
 
 def sp12v_normalize(netname, edge=""):
-    m = re.match("sp12_v_([bt])_(\d+)$", netname)
+    m = re_match_cached("sp12_v_([bt])_(\d+)$", netname)
     assert m
     if not m: return None
     cur_edge = m.group(1)
@@ -1197,18 +1225,18 @@
     netname = netname.replace("wire_con_box/", "")
     netname = netname.replace("wire_bram/", "")
     if (ramb or ramt or ramb_8k or ramt_8k) and netname.startswith("input"):
-        match = re.match(r"input(\d)_(\d)", netname)
+        match = re_match_cached(r"input(\d)_(\d)", netname)
         idx1, idx2 = (int(match.group(1)), int(match.group(2)))
         if ramb: netname="ram/WADDR_%d" % (idx1*4 + idx2)
         if ramt: netname="ram/RADDR_%d" % (idx1*4 + idx2)
         if ramb_8k: netname="ram/RADDR_%d" % ([7, 6, 5, 4, 3, 2, 1, 0, -1, -1, -1, -1, -1, 10, 9, 8][idx1*4 + idx2])
         if ramt_8k: netname="ram/WADDR_%d" % ([7, 6, 5, 4, 3, 2, 1, 0, -1, -1, -1, -1, -1, 10, 9, 8][idx1*4 + idx2])
-    match = re.match(r"(...)_op_(.*)", netname)
+    match = re_match_cached(r"(...)_op_(.*)", netname)
     if match and (match.group(1) != "slf"):
         netname = "neigh_op_%s_%s" % (match.group(1), match.group(2))
-    if re.match(r"lutff_7/(cen|clk|s_r)", netname):
+    if re_match_cached(r"lutff_7/(cen|clk|s_r)", netname):
         netname = netname.replace("lutff_7/", "lutff_global/")
-    if re.match(r"io_1/(cen|inclk|outclk)", netname):
+    if re_match_cached(r"io_1/(cen|inclk|outclk)", netname):
         netname = netname.replace("io_1/", "io_global/")
     if netname == "carry_in_mux/cout":
         return "carry_in_mux"
@@ -1216,113 +1244,113 @@
 
 def pos_has_net(pos, netname):
     if pos in ("l", "r"):
-        if re.search(r"_vert_\d+$", netname): return False
-        if re.search(r"_horz_[rl]_\d+$", netname): return False
+        if re_search_cached(r"_vert_\d+$", netname): return False
+        if re_search_cached(r"_horz_[rl]_\d+$", netname): return False
     if pos in ("t", "b"):
-        if re.search(r"_horz_\d+$", netname): return False
-        if re.search(r"_vert_[bt]_\d+$", netname): return False
+        if re_search_cached(r"_horz_\d+$", netname): return False
+        if re_search_cached(r"_vert_[bt]_\d+$", netname): return False
     return True
 
 def pos_follow_net(pos, direction, netname, is_ultra):
     if pos == "x" or ((pos in ("l", "r")) and is_ultra):
-            m = re.match("sp4_h_[lr]_(\d+)$", netname)
+            m = re_match_cached("sp4_h_[lr]_(\d+)$", netname)
             if m and direction in ("l", "L"):
                 n = sp4h_normalize(netname, "l")
                 if n is not None:
                     if direction == "l" or is_ultra:
-                        n = re.sub("_l_", "_r_", n)
+                        n = re_sub_cached("_l_", "_r_", n)
                         n = sp4h_normalize(n)
                     else:
-                        n = re.sub("_l_", "_", n)
-                        n = re.sub("sp4_h_", "span4_horz_", n)
+                        n = re_sub_cached("_l_", "_", n)
+                        n = re_sub_cached("sp4_h_", "span4_horz_", n)
                     return n
             if m and direction in ("r", "R"):
                 n = sp4h_normalize(netname, "r")
                 if n is not None:
                     if direction == "r" or is_ultra:
-                        n = re.sub("_r_", "_l_", n)
+                        n = re_sub_cached("_r_", "_l_", n)
                         n = sp4h_normalize(n)
                     else:
-                        n = re.sub("_r_", "_", n)
-                        n = re.sub("sp4_h_", "span4_horz_", n)
+                        n = re_sub_cached("_r_", "_", n)
+                        n = re_sub_cached("sp4_h_", "span4_horz_", n)
                     return n
 
-            m = re.match("sp4_v_[tb]_(\d+)$", netname)
+            m = re_match_cached("sp4_v_[tb]_(\d+)$", netname)
             if m and direction in ("t", "T"):
                 n = sp4v_normalize(netname, "t")
                 if n is not None:
                     if is_ultra and direction == "T" and pos in ("l", "r"):
-                        return re.sub("sp4_v_", "span4_vert_", n)
+                        return re_sub_cached("sp4_v_", "span4_vert_", n)
                     elif direction == "t":
-                        n = re.sub("_t_", "_b_", n)
+                        n = re_sub_cached("_t_", "_b_", n)
                         n = sp4v_normalize(n)
                     else:
-                        n = re.sub("_t_", "_", n)
-                        n = re.sub("sp4_v_", "span4_vert_", n)
+                        n = re_sub_cached("_t_", "_", n)
+                        n = re_sub_cached("sp4_v_", "span4_vert_", n)
                     return n
             if m and direction in ("b", "B"):
                 n = sp4v_normalize(netname, "b")
                 if n is not None:
                     if is_ultra and direction == "B" and pos in ("l", "r"):
-                        return re.sub("sp4_v_", "span4_vert_", n)
+                        return re_sub_cached("sp4_v_", "span4_vert_", n)
                     elif direction == "b":
-                        n = re.sub("_b_", "_t_", n)
+                        n = re_sub_cached("_b_", "_t_", n)
                         n = sp4v_normalize(n)
                     else:
-                        n = re.sub("_b_", "_", n)
-                        n = re.sub("sp4_v_", "span4_vert_", n)
+                        n = re_sub_cached("_b_", "_", n)
+                        n = re_sub_cached("sp4_v_", "span4_vert_", n)
                     return n
 
-            m = re.match("sp12_h_[lr]_(\d+)$", netname)
+            m = re_match_cached("sp12_h_[lr]_(\d+)$", netname)
             if m and direction in ("l", "L"):
                 n = sp12h_normalize(netname, "l")
                 if n is not None:
                     if direction == "l" or is_ultra:
-                        n = re.sub("_l_", "_r_", n)
+                        n = re_sub_cached("_l_", "_r_", n)
                         n = sp12h_normalize(n)
                     else:
-                        n = re.sub("_l_", "_", n)
-                        n = re.sub("sp12_h_", "span12_horz_", n)
+                        n = re_sub_cached("_l_", "_", n)
+                        n = re_sub_cached("sp12_h_", "span12_horz_", n)
                     return n
             if m and direction in ("r", "R"):
                 n = sp12h_normalize(netname, "r")
                 if n is not None:
                     if direction == "r" or is_ultra:
-                        n = re.sub("_r_", "_l_", n)
+                        n = re_sub_cached("_r_", "_l_", n)
                         n = sp12h_normalize(n)
                     else:
-                        n = re.sub("_r_", "_", n)
-                        n = re.sub("sp12_h_", "span12_horz_", n)
+                        n = re_sub_cached("_r_", "_", n)
+                        n = re_sub_cached("sp12_h_", "span12_horz_", n)
                     return n
 
-            m = re.match("sp12_v_[tb]_(\d+)$", netname)
+            m = re_match_cached("sp12_v_[tb]_(\d+)$", netname)
             if m and direction in ("t", "T"):
                 n = sp12v_normalize(netname, "t")
                 if n is not None:
                     if direction == "t":
-                        n = re.sub("_t_", "_b_", n)
+                        n = re_sub_cached("_t_", "_b_", n)
                         n = sp12v_normalize(n)
                     elif direction == "T" and pos in ("l", "r"):
                         pass
                     else:
-                        n = re.sub("_t_", "_", n)
-                        n = re.sub("sp12_v_", "span12_vert_", n)
+                        n = re_sub_cached("_t_", "_", n)
+                        n = re_sub_cached("sp12_v_", "span12_vert_", n)
                     return n
             if m and direction in ("b", "B"):
                 n = sp12v_normalize(netname, "b")
                 if n is not None:
                     if direction == "b":
-                        n = re.sub("_b_", "_t_", n)
+                        n = re_sub_cached("_b_", "_t_", n)
                         n = sp12v_normalize(n)
                     elif direction == "B" and pos in ("l", "r"):
                         pass
                     else:
-                        n = re.sub("_b_", "_", n)
-                        n = re.sub("sp12_v_", "span12_vert_", n)
+                        n = re_sub_cached("_b_", "_", n)
+                        n = re_sub_cached("sp12_v_", "span12_vert_", n)
                     return n
 
     if (pos in ("l", "r" )) and (not is_ultra):
-        m = re.match("span4_vert_([bt])_(\d+)$", netname)
+        m = re_match_cached("span4_vert_([bt])_(\d+)$", netname)
         if m:
             case, idx = direction + m.group(1), int(m.group(2))
             if case == "tt":
@@ -1335,7 +1363,7 @@
                 return "span4_vert_t_%d" % idx
 
     if pos in ("t", "b" ):
-        m = re.match("span4_horz_([rl])_(\d+)$", netname)
+        m = re_match_cached("span4_horz_([rl])_(\d+)$", netname)
         if m:
             case, idx = direction + m.group(1), int(m.group(2))
             if direction == "L":
@@ -1352,27 +1380,27 @@
                 return "span4_horz_l_%d" % idx
 
     if pos == "l" and direction == "r" and (not is_ultra):
-            m = re.match("span4_horz_(\d+)$", netname)
+            m = re_match_cached("span4_horz_(\d+)$", netname)
             if m: return sp4h_normalize("sp4_h_l_%s" % m.group(1))
-            m = re.match("span12_horz_(\d+)$", netname)
+            m = re_match_cached("span12_horz_(\d+)$", netname)
             if m: return sp12h_normalize("sp12_h_l_%s" % m.group(1))
 
     if pos == "r" and direction == "l" and (not is_ultra):
-            m = re.match("span4_horz_(\d+)$", netname)
+            m = re_match_cached("span4_horz_(\d+)$", netname)
             if m: return sp4h_normalize("sp4_h_r_%s" % m.group(1))
-            m = re.match("span12_horz_(\d+)$", netname)
+            m = re_match_cached("span12_horz_(\d+)$", netname)
             if m: return sp12h_normalize("sp12_h_r_%s" % m.group(1))
 
     if pos == "t" and direction == "b":
-            m = re.match("span4_vert_(\d+)$", netname)
+            m = re_match_cached("span4_vert_(\d+)$", netname)
             if m: return sp4v_normalize("sp4_v_t_%s" % m.group(1))
-            m = re.match("span12_vert_(\d+)$", netname)
+            m = re_match_cached("span12_vert_(\d+)$", netname)
             if m: return sp12v_normalize("sp12_v_t_%s" % m.group(1))
 
     if pos == "b" and direction == "t":
-            m = re.match("span4_vert_(\d+)$", netname)
+            m = re_match_cached("span4_vert_(\d+)$", netname)
             if m: return sp4v_normalize("sp4_v_b_%s" % m.group(1))
-            m = re.match("span12_vert_(\d+)$", netname)
+            m = re_match_cached("span12_vert_(\d+)$", netname)
             if m: return sp12v_normalize("sp12_v_b_%s" % m.group(1))
 
     return None
@@ -1405,7 +1433,7 @@
     return tile[0][0]
 
 def key_netname(netname):
-    return re.sub(r"\d+", lambda m: "%09d" % int(m.group(0)), netname)
+    return re_sub_cached(r"\d+", lambda m: "%09d" % int(m.group(0)), netname)
 
 def run_checks_neigh():
     print("Running consistency checks on neighbour finder..")
diff --git a/icebox/icebox_asc2hlc.py b/icebox/icebox_asc2hlc.py
index facca4b..003106f 100755
--- a/icebox/icebox_asc2hlc.py
+++ b/icebox/icebox_asc2hlc.py
@@ -15,6 +15,7 @@
 
 import getopt, os, re, sys
 import icebox
+from icebox import re_match_cached
 
 GLB_NETWK_EXTERNAL_BLOCKS = [(13, 8, 1), (0, 8, 1), (7, 17, 0), (7, 0, 0),
                              (0, 9, 0), (13, 9, 0), (6, 0, 1), (6, 17, 1)]
@@ -63,24 +64,24 @@
 
     # logic and RAM tiles
 
-    match = re.match(r'sp4_h_r_(\d+)$', net)
+    match = re_match_cached(r'sp4_h_r_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 12)
         return 'span4_y%d_g%d_%d' % (y, x - g + 4, i)
-    match = re.match(r'sp4_h_l_(\d+)$', net)
+    match = re_match_cached(r'sp4_h_l_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 12)
         return 'span4_y%d_g%d_%d' % (y, x - g + 3, i)
 
-    match = re.match(r'sp4_v_b_(\d+)$', net)
+    match = re_match_cached(r'sp4_v_b_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 12)
         return 'span4_x%d_g%d_%d' % (x, y + g, i)
-    match = re.match(r'sp4_v_t_(\d+)$', net)
+    match = re_match_cached(r'sp4_v_t_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 12)
         return 'span4_x%d_g%d_%d' % (x, y + g + 1, i)
-    match = re.match(r'sp4_r_v_b_(\d+)$', net)
+    match = re_match_cached(r'sp4_r_v_b_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 12)
         if x == fw:
@@ -89,27 +90,27 @@
         else:
             return 'span4_x%d_g%d_%d' % (x + 1, y + g, i)
 
-    match = re.match(r'sp12_h_r_(\d+)$', net)
+    match = re_match_cached(r'sp12_h_r_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 2)
         return 'span12_y%d_g%d_%d' % (y, x - g + 12, i)
-    match = re.match(r'sp12_h_l_(\d+)$', net)
+    match = re_match_cached(r'sp12_h_l_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 2)
         return 'span12_y%d_g%d_%d' % (y, x - g + 11, i)
 
-    match = re.match(r'sp12_v_b_(\d+)$', net)
+    match = re_match_cached(r'sp12_v_b_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 2)
         return 'span12_x%d_g%d_%d' % (x, y + g, i)
-    match = re.match(r'sp12_v_t_(\d+)$', net)
+    match = re_match_cached(r'sp12_v_t_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 2)
         return 'span12_x%d_g%d_%d' % (x, y + g + 1, i)
 
     # I/O tiles
 
-    match = re.match(r'span4_horz_(\d+)$', net)
+    match = re_match_cached(r'span4_horz_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 12)
         if x == 0:
@@ -117,7 +118,7 @@
         else:
             return 'span4_y%d_g%d_%d' % (y, x - g + 3, i)
 
-    match = re.match(r'span4_vert_(\d+)$', net)
+    match = re_match_cached(r'span4_vert_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 12)
         if y == 0:
@@ -125,7 +126,7 @@
         else:
             return 'span4_x%d_g%d_%d' % (x, y + g, i)
 
-    match = re.match(r'span12_horz_(\d+)$', net)
+    match = re_match_cached(r'span12_horz_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 2)
         if x == 0:
@@ -133,7 +134,7 @@
         else:
             return 'span12_y%d_g%d_%d' % (y, x - g + 11, i)
 
-    match = re.match(r'span12_vert_(\d+)$', net)
+    match = re_match_cached(r'span12_vert_(\d+)$', net)
     if match is not None:
         g, i = group_and_index(match.group(1), 2)
         if y == 0:
@@ -143,7 +144,7 @@
 
     # I/O tiles - peripheral wires
 
-    match = re.match(r'span4_horz_r_(\d+)$', net)
+    match = re_match_cached(r'span4_horz_r_(\d+)$', net)
     if match is not None:
         n = int(match.group(1)); g = n // 4; i = n % 4
         if y == 0:
@@ -161,7 +162,7 @@
             else:
                 return 'span4_top_g%d_%d' % (x + 4 - g, i)
 
-    match = re.match(r'span4_horz_l_(\d+)$', net)
+    match = re_match_cached(r'span4_horz_l_(\d+)$', net)
     if match is not None:
         n = int(match.group(1)); g = n // 4; i = n % 4
         if y == 0:
@@ -175,7 +176,7 @@
             else:
                 return 'span4_top_g%d_%d' % (x + 3 - g, i)
 
-    match = re.match(r'span4_vert_b_(\d+)$', net)
+    match = re_match_cached(r'span4_vert_b_(\d+)$', net)
     if match is not None:
         n = int(match.group(1)); g = n // 4; i = n % 4
         if x == 0:
@@ -193,7 +194,7 @@
             else:
                 return 'span4_right_g%d_%d' % (y + g, i)
 
-    match = re.match(r'span4_vert_t_(\d+)$', net)
+    match = re_match_cached(r'span4_vert_t_(\d+)$', net)
     if match is not None:
         n = int(match.group(1)); g = n // 4; i = n % 4
         if x == 0:
@@ -740,7 +741,7 @@
         for entry in db:
             # LC bits don't have a useful entry in the database; skip them
             # for now
-            if re.match(r'LC_', entry[1]):
+            if re_match_cached(r'LC_', entry[1]):
                 continue
 
             # some nets have different names depending on the tile; filter
diff --git a/icebox/icebox_colbuf.py b/icebox/icebox_colbuf.py
index 26b8940..ec6843e 100755
--- a/icebox/icebox_colbuf.py
+++ b/icebox/icebox_colbuf.py
@@ -16,6 +16,7 @@
 #
 
 import icebox
+from icebox import re_match_cached
 import getopt, sys, re
 
 check_mode = False
@@ -70,7 +71,7 @@
                 if bit.startswith("!"):
                     value = "0"
                     bit = bit[1:]
-                match = re.match("B([0-9]+)\[([0-9]+)\]", bit)
+                match = re_match_cached("B([0-9]+)\[([0-9]+)\]", bit)
                 cache_entry[1].append((int(match.group(1)), int(match.group(2)), value))
             cache.append(cache_entry)
     return cache
@@ -120,7 +121,7 @@
     tile_db = ic.tile_db(tile[0], tile[1])
     for entry in tile_db:
         if entry[1] == "ColBufCtrl" and entry[2] == "glb_netwk_%d" % bit:
-            match = re.match("B([0-9]+)\[([0-9]+)\]", entry[0][0])
+            match = re_match_cached("B([0-9]+)\[([0-9]+)\]", entry[0][0])
             l = tile_dat[int(match.group(1))]
             n = int(match.group(2))
             l = l[:n] + value + l[n+1:]
diff --git a/icebox/icebox_diff.py b/icebox/icebox_diff.py
index c0db200..5252fc4 100755
--- a/icebox/icebox_diff.py
+++ b/icebox/icebox_diff.py
@@ -16,6 +16,7 @@
 #
 
 import icebox
+from icebox import re_match_cached
 import sys
 import re
 
@@ -54,7 +55,7 @@
                 bits.add("!B%d[%d]" % (k, i))
     text = set()
     for entry in db:
-        if re.match(r"LC_", entry[1]):
+        if re_match_cached(r"LC_", entry[1]):
             continue
         if entry[1] in ("routing", "buffer"):
             continue
diff --git a/icebox/icebox_explain.py b/icebox/icebox_explain.py
index 4e678ff..f843c09 100755
--- a/icebox/icebox_explain.py
+++ b/icebox/icebox_explain.py
@@ -16,6 +16,7 @@
 #
 
 import icebox
+from icebox import re_match_cached, re_search_cached
 import getopt, sys, re
 
 print_bits = False
@@ -82,14 +83,14 @@
             else:
                 bits.add("!B%d[%d]" % (k, i))
 
-    if re.search(r"logic_tile", stmt):
+    if re_search_cached(r"logic_tile", stmt):
         active_luts = set([i for i in range(8) if "1" in icebox.get_lutff_bits(tile, i)])
 
     text = set()
     used_lc = set()
     text_default_mask = 0
     for entry in db:
-        if re.match(r"LC_", entry[1]):
+        if re_match_cached(r"LC_", entry[1]):
             continue
         if entry[1] in ("routing", "buffer"):
             if not ic.tile_has_net(x, y, entry[2]): continue
@@ -117,7 +118,7 @@
         bitinfo.append("")
         extra_text = ""
         for i in range(len(line)):
-            if 36 <= i <= 45 and re.search(r"(logic_tile|dsp\d_tile|ipcon_tile)", stmt):
+            if 36 <= i <= 45 and re_search_cached(r"(logic_tile|dsp\d_tile|ipcon_tile)", stmt):
                 lutff_idx = k // 2
                 lutff_bitnum = (i-36) + 10*(k%2)
                 if line[i] == "1":
diff --git a/icebox/icebox_hlc2asc.py b/icebox/icebox_hlc2asc.py
index 8b82132..a95f610 100755
--- a/icebox/icebox_hlc2asc.py
+++ b/icebox/icebox_hlc2asc.py
@@ -15,6 +15,7 @@
 
 import getopt, os, re, sys
 import icebox
+from icebox import re_match_cached, re_sub_cached
 
 
 ## Get the tile-local name of a net.
@@ -32,7 +33,7 @@
             i = i + 1 - (i % 2) * 2
         return g * group_size + i
 
-    match = re.match(r'span4_y(\d+)_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span4_y(\d+)_g(\d+)_(\d+)$', net)
     if match is not None:
         my = int(match.group(1))
         mw = int(match.group(2))
@@ -53,7 +54,7 @@
         else:
             return 'sp4_h_r_%d' % index(mg, mi, 12)
 
-    match = re.match(r'span4_x(\d+)_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span4_x(\d+)_g(\d+)_(\d+)$', net)
     if match is not None:
         mx = int(match.group(1))
         mw = int(match.group(2))
@@ -77,7 +78,7 @@
         else:
             return 'sp4_v_b_%d' % index(mg, mi, 12)
 
-    match = re.match(r'dummy_y(\d+)_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'dummy_y(\d+)_g(\d+)_(\d+)$', net)
     if match is not None:
         my = int(match.group(1))
         mw = int(match.group(2))
@@ -89,7 +90,7 @@
 
         return 'sp4_r_v_b_%d' % index(mg, mi, 12)
 
-    match = re.match(r'span12_y(\d+)_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span12_y(\d+)_g(\d+)_(\d+)$', net)
     if match is not None:
         my = int(match.group(1))
         mw = int(match.group(2))
@@ -110,7 +111,7 @@
         else:
             return 'sp12_h_r_%d' % index(mg, mi, 2)
 
-    match = re.match(r'span12_x(\d+)_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span12_x(\d+)_g(\d+)_(\d+)$', net)
     if match is not None:
         mx = int(match.group(1))
         mw = int(match.group(2))
@@ -131,7 +132,7 @@
         else:
             return 'sp12_v_b_%d' % index(mg, mi, 2)
 
-    match = re.match(r'span4_bottom_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span4_bottom_g(\d+)_(\d+)$', net)
     if match is not None:
         mw = int(match.group(1))
         mi = int(match.group(2))
@@ -153,7 +154,7 @@
                 assert fw - x + mg - 4 >= 0
                 return 'span4_horz_r_%d' % (mg * 4 + mi)
 
-    match = re.match(r'span4_left_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span4_left_g(\d+)_(\d+)$', net)
     if match is not None:
         mw = int(match.group(1))
         mi = int(match.group(2))
@@ -181,7 +182,7 @@
                 assert y + mg - 3 >= 0
                 return 'span4_vert_b_%d' % (mg * 4 + mi)
 
-    match = re.match(r'span4_right_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span4_right_g(\d+)_(\d+)$', net)
     if match is not None:
         mw = int(match.group(1))
         mi = int(match.group(2))
@@ -204,7 +205,7 @@
             assert y + mg < fh + 3
             return 'span4_vert_b_%d' % (mg * 4 + mi)
 
-    match = re.match(r'span4_top_g(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span4_top_g(\d+)_(\d+)$', net)
     if match is not None:
         mw = int(match.group(1))
         mi = int(match.group(2))
@@ -231,7 +232,7 @@
             assert x - mg + 1 < fw
             return 'span4_horz_r_%d' % (mg * 4 + mi)
 
-    match = re.match(r'span4_bottomright(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span4_bottomright(\d+)_(\d+)$', net)
     if match is not None:
         mw = int(match.group(1))
         mi = int(match.group(2))
@@ -249,7 +250,7 @@
             assert y + mg - 5 < 0
             return 'span4_vert_b_%d' % (mg * 4 + mi)
 
-    match = re.match(r'span4_topleft(\d+)_(\d+)$', net)
+    match = re_match_cached(r'span4_topleft(\d+)_(\d+)$', net)
     if match is not None:
         mw = int(match.group(1))
         mi = int(match.group(2))
@@ -506,9 +507,9 @@
 
 def parse_verilog_bitvector_to_bits(in_str):
     #replace x with 0
-    in_str = re.sub('[xX]', '0', in_str)
+    in_str = re_sub_cached('[xX]', '0', in_str)
 
-    m = re.match("([0-9]+)'([hdob])([0-9a-fA-F]+)", in_str)
+    m = re_match_cached("([0-9]+)'([hdob])([0-9a-fA-F]+)", in_str)
     if m:
         num_bits = int(m.group(1))
         prefix = m.group(2)
@@ -773,7 +774,7 @@
         bits_clear = set()
 
         for bit in bits:
-            match = re.match(r'(!?)B(\d+)\[(\d+)\]$', bit)
+            match = re_match_cached(r'(!?)B(\d+)\[(\d+)\]$', bit)
             if not match:
                 raise ValueError("invalid bit description: %s" % bit)
             if match.group(1):
@@ -878,7 +879,7 @@
         if fields[0] == 'lut' and len(fields) == 2:
             self.lut_bits = fields[1]
         elif fields[0] == 'out' and len(fields) >= 3 and fields[1] == '=':
-            m = re.match("([0-9]+)'b([01]+)", fields[2])
+            m = re_match_cached("([0-9]+)'b([01]+)", fields[2])
             if m:
                 lut_bits = parse_verilog_bitvector_to_bits(fields[2])
                 # Verilog 16'bXXXX is MSB first but the bitstream wants LSB.
diff --git a/icebox/icebox_html.py b/icebox/icebox_html.py
index 6415230..b710f61 100755
--- a/icebox/icebox_html.py
+++ b/icebox/icebox_html.py
@@ -16,6 +16,7 @@
 #
 
 import icebox
+from icebox import re_match_cached, re_sub_cached
 import getopt, sys, os, re
 
 chipname = "iCE40 HX1K"
@@ -252,7 +253,7 @@
         if not ic.tile_has_entry(tx, ty, entry):
             continue
         for bit in [bit.replace("!", "") for bit in entry[0]]:
-            match = re.match(r"B(\d+)\[(\d+)\]$", bit)
+            match = re_match_cached(r"B(\d+)\[(\d+)\]$", bit)
             idx1 = int(match.group(1))
             idx2 = int(match.group(2))
             if entry[1] == "routing":
@@ -324,7 +325,7 @@
         for s in segs:
             if s[0] == tx and s[1] == ty:
                 this_segs.append(s[2])
-                match = re.match(r"(.*?_)(\d+)(.*)", s[2])
+                match = re_match_cached(r"(.*?_)(\d+)(.*)", s[2])
                 if match:
                     this_tile_nets.setdefault(match.group(1) + "*" + match.group(3), set()).add(int(match.group(2)))
                 else:
@@ -444,7 +445,7 @@
         for cfggrp in sorted(grpgrp[cat]):
             grp = config_groups[cfggrp]
             for bit in cfggrp.split(",")[1:]:
-                match = re.match(r"B(\d+)\[(\d+)\]", bit)
+                match = re_match_cached(r"B(\d+)\[(\d+)\]", bit)
                 bits_in_cat.add((int(match.group(1)), int(match.group(2))))
 
         print('<table style="font-size:x-small">')
@@ -520,7 +521,7 @@
             bits = cfggrp.split(",")[1:]
             print('<p><table style="font-size:small" border><tr>')
             for bit in bits:
-                print('<th style="width:5em"><a name="%s">%s</a></th>' % (re.sub(r"B(\d+)\[(\d+)\]", r"B.\1.\2", bit), bit))
+                print('<th style="width:5em"><a name="%s">%s</a></th>' % (re_sub_cached(r"B(\d+)\[(\d+)\]", r"B.\1.\2", bit), bit))
             group_lines = list()
             is_buffer = True
             for entry in grp:
@@ -569,7 +570,7 @@
 
     print('<p><table style="font-size:small" border><tr><th>Function</th><th>Bits</th></tr>')
     for cfggrp in sorted(other_config_groups):
-        bits = " ".join(['<a name="%s">%s</a>' % (re.sub(r"B(\d+)\[(\d+)\]", r"B.\1.\2", bit), bit) for bit in sorted(other_config_groups[cfggrp])])
+        bits = " ".join(['<a name="%s">%s</a>' % (re_sub_cached(r"B(\d+)\[(\d+)\]", r"B.\1.\2", bit), bit) for bit in sorted(other_config_groups[cfggrp])])
         cfggrp = cfggrp.replace("&nbsp;" + list(other_config_groups[cfggrp])[0], "")
         print('<tr><td>%s</td><td>%s</td></tr>' % (cfggrp, bits))
     print('</table></p>')
diff --git a/icebox/icebox_maps.py b/icebox/icebox_maps.py
index c791bea..35ff316 100755
--- a/icebox/icebox_maps.py
+++ b/icebox/icebox_maps.py
@@ -16,6 +16,7 @@
 #
 
 import icebox
+from icebox import re_match_cached
 import getopt, sys, re
 
 mode = None
@@ -58,7 +59,7 @@
                 funcs.add("r")
             elif entry[1] == "buffer":
                 funcs.add("b")
-            elif re.match("LC_", entry[1]):
+            elif re_match_cached("LC_", entry[1]):
                 funcs.add("l")
             elif entry[1] == "NegClk":
                 funcs.add("N")
@@ -94,7 +95,7 @@
             if icebox.pos_has_net(pos[0], entry[3]): netnames.add(entry[3])
     last_prefix = ""
     for net in sorted(netnames, key=icebox.key_netname):
-        match = re.match(r"(.*?)(\d+)$", net)
+        match = re_match_cached(r"(.*?)(\d+)$", net)
         if match:
             if last_prefix == match.group(1):
                 print(",%s" % match.group(2), end="")
diff --git a/icebox/icebox_stat.py b/icebox/icebox_stat.py
index ffb7b6a..ec404fb 100755
--- a/icebox/icebox_stat.py
+++ b/icebox/icebox_stat.py
@@ -16,6 +16,7 @@
 #
 
 import icebox
+from icebox import re_match_cached
 import getopt, sys, re
 
 verbose = False
@@ -72,23 +73,23 @@
 for segs in connections:
     for seg in segs:
         if ic.tile_type(seg[0], seg[1]) == "IO" and seg[2].startswith("io_"):
-            match = re.match("io_(\d+)/D_(IN|OUT)_(\d+)", seg[2])
+            match = re_match_cached("io_(\d+)/D_(IN|OUT)_(\d+)", seg[2])
             if match:
                 loc = (seg[0], seg[1], int(match.group(1)))
                 io_locations.add(loc)
 
         if ic.tile_type(seg[0], seg[1]) == "LOGIC" and seg[2].startswith("lutff_"):
-            match = re.match("lutff_(\d)/in_\d", seg[2])
+            match = re_match_cached("lutff_(\d)/in_\d", seg[2])
             if match:
                 loc = (seg[0], seg[1], int(match.group(1)))
                 lut_locations.add(loc)
 
-            match = re.match("lutff_(\d)/cout", seg[2])
+            match = re_match_cached("lutff_(\d)/cout", seg[2])
             if match:
                 loc = (seg[0], seg[1], int(match.group(1)))
                 carry_locations.add(loc)
 
-            match = re.match("lutff_(\d)/out", seg[2])
+            match = re_match_cached("lutff_(\d)/out", seg[2])
             if match:
                 loc = (seg[0], seg[1], int(match.group(1)))
                 seq_bits = icebox.get_lutff_seq_bits(ic.tile(loc[0], loc[1]), loc[2])
@@ -100,7 +101,7 @@
             bram_locations.add(loc)
 
         if seg[2].startswith("glb_netwk_"):
-            match = re.match("glb_netwk_(\d)", seg[2])
+            match = re_match_cached("glb_netwk_(\d)", seg[2])
             if match:
                 global_nets.add(int(match.group(1)))
 
@@ -108,7 +109,7 @@
 
 for entry in icebox.iotile_l_db:
     if entry[1] == "PLL":
-        match = re.match(r"B(\d+)\[(\d+)\]", entry[0][0]);
+        match = re_match_cached(r"B(\d+)\[(\d+)\]", entry[0][0]);
         assert match
         pll_config_bitidx[entry[2]] = (int(match.group(1)), int(match.group(2)))
 
diff --git a/icebox/icebox_vlog.py b/icebox/icebox_vlog.py
index 64e9ea5..184cb03 100755
--- a/icebox/icebox_vlog.py
+++ b/icebox/icebox_vlog.py
@@ -16,6 +16,7 @@
 #
 
 import icebox
+from icebox import re_match_cached, re_sub_cached, re_search_cached
 import getopt, sys, re
 
 strip_comments = False
@@ -93,22 +94,22 @@
     elif o in ("-p", "-P"):
         with open(a, "r") as f:
             for line in f:
-                if o == "-P" and not re.search(" # ICE_(GB_)?IO", line):
+                if o == "-P" and not re_search_cached(" # ICE_(GB_)?IO", line):
                     continue
-                line = re.sub(r"#.*", "", line.strip()).split()
+                line = re_sub_cached(r"#.*", "", line.strip()).split()
                 if "--warn-no-port" in line:
                     line.remove("--warn-no-port")
                 if len(line) and line[0] == "set_io":
                     p = line[1]
                     if o == "-P":
                         p = p.lower()
-                        p = re.sub(r"_ibuf$", "", p)
-                        p = re.sub(r"_obuft$", "", p)
-                        p = re.sub(r"_obuf$", "", p)
-                        p = re.sub(r"_gb_io$", "", p)
-                        p = re.sub(r"_pad(_[0-9]+|)$", r"\1", p)
+                        p = re_sub_cached(r"_ibuf$", "", p)
+                        p = re_sub_cached(r"_obuft$", "", p)
+                        p = re_sub_cached(r"_obuf$", "", p)
+                        p = re_sub_cached(r"_gb_io$", "", p)
+                        p = re_sub_cached(r"_pad(_[0-9]+|)$", r"\1", p)
                     portnames.add(p)
-                    if not re.match(r"[a-zA-Z_][a-zA-Z0-9_]*$", p):
+                    if not re_match_cached(r"[a-zA-Z_][a-zA-Z0-9_]*$", p):
                         p = "\\%s " % p
                     unmatched_ports.add(p)
                     if len(line) > 3:
@@ -177,7 +178,7 @@
 
 for entry in icebox.iotile_l_db:
     if entry[1] == "PLL":
-        match = re.match(r"B(\d+)\[(\d+)\]", entry[0][0]);
+        match = re_match_cached(r"B(\d+)\[(\d+)\]", entry[0][0]);
         assert match
         pll_config_bitidx[entry[2]] = (int(match.group(1)), int(match.group(2)))
 
@@ -234,8 +235,8 @@
             iocells_negclk.add((idx[0], idx[1], 0))
             iocells_negclk.add((idx[0], idx[1], 1))
         if entry[1].startswith("IOB_") and entry[2].startswith("PINTYPE_") and tc.match(entry[0]):
-            match1 = re.match("IOB_(\d+)", entry[1])
-            match2 = re.match("PINTYPE_(\d+)", entry[2])
+            match1 = re_match_cached("IOB_(\d+)", entry[1])
+            match2 = re_match_cached("PINTYPE_(\d+)", entry[2])
             assert match1 and match2
             iocells_type[(idx[0], idx[1], int(match1.group(1)))][int(match2.group(1))] = "1"
     iocells_type[(idx[0], idx[1], 0)] = "".join(iocells_type[(idx[0], idx[1], 0)])
@@ -244,7 +245,7 @@
 for segs in sorted(ic.group_segments()):
     for seg in segs:
         if ic.tile_type(seg[0], seg[1]) == "IO":
-            match = re.match("io_(\d+)/D_(IN|OUT)_(\d+)", seg[2])
+            match = re_match_cached("io_(\d+)/D_(IN|OUT)_(\d+)", seg[2])
             if match:
                 cell = (seg[0], seg[1], int(match.group(1)))
                 if cell in iocells_skip:
@@ -287,7 +288,7 @@
     renamed_net_to_port = False
 
     for s in segs:
-        match =  re.match("io_(\d+)/PAD", s[2])
+        match =  re_match_cached("io_(\d+)/PAD", s[2])
         if match:
             idx = (s[0], s[1], int(match.group(1)))
             p = "io_%d_%d_%d" % idx
@@ -322,7 +323,7 @@
                 text_ports.append("inout %s" % p)
                 text_wires.append("assign %s = %s;" % (p, n))
 
-        match =  re.match("lutff_(\d+)/", s[2])
+        match =  re_match_cached("lutff_(\d+)/", s[2])
         if match:
             #IpCon and DSP tiles look like logic tiles, but aren't.
             if ic.device in ["5k", "u4k"] and (s[0] == 0 or s[0] == ic.max_x):
@@ -347,10 +348,10 @@
 
     count_drivers = []
     for s in segs:
-        if re.match(r"ram/RDATA_", s[2]): count_drivers.append(s[2])
-        if re.match(r"io_./D_IN_", s[2]): count_drivers.append(s[2])
-        if re.match(r"lutff_./out", s[2]): count_drivers.append(s[2])
-        if re.match(r"lutff_./lout", s[2]): count_drivers.append(s[2])
+        if re_match_cached(r"ram/RDATA_", s[2]): count_drivers.append(s[2])
+        if re_match_cached(r"io_./D_IN_", s[2]): count_drivers.append(s[2])
+        if re_match_cached(r"lutff_./out", s[2]): count_drivers.append(s[2])
+        if re_match_cached(r"lutff_./lout", s[2]): count_drivers.append(s[2])
 
     if len(count_drivers) != 1 and check_driver:
         failed_drivers_check.append((n, count_drivers))
@@ -870,7 +871,7 @@
     vec_ports_max = dict()
     vec_ports_dir = dict()
     for port in text_ports:
-        match = re.match(r"(input|output|inout) (.*)\[(\d+)\] ?$", port);
+        match = re_match_cached(r"(input|output|inout) (.*)\[(\d+)\] ?$", port);
         if match:
             vec_ports_min[match.group(2)] = min(vec_ports_min.setdefault(match.group(2), int(match.group(3))), int(match.group(3)))
             vec_ports_max[match.group(2)] = max(vec_ports_max.setdefault(match.group(2), int(match.group(3))), int(match.group(3)))
@@ -889,7 +890,7 @@
 new_text_regs = list()
 new_text_raw = list()
 for line in text_wires:
-    match = re.match(r"wire ([^ ;]+)(.*)", line)
+    match = re_match_cached(r"wire ([^ ;]+)(.*)", line)
     if match:
         if strip_comments:
             name = match.group(1)
diff --git a/icetime/.gitignore b/icetime/.gitignore
index 5c1b012..70d5a6b 100644
--- a/icetime/.gitignore
+++ b/icetime/.gitignore
@@ -1,6 +1,7 @@
 icetime
 icetime.exe
 timings.inc
+timings-*.cc
 test[0-9]*
 *.d
 *.o
diff --git a/icetime/Makefile b/icetime/Makefile
index 6d9ac4f..38e2fe6 100644
--- a/icetime/Makefile
+++ b/icetime/Makefile
@@ -65,7 +65,7 @@
 show: show0 show1 show2 show3 show4 show5 show6 show7 show8 show9
 
 clean:
-	rm -f icetime$(EXE) icetime.exe *.o *.d
+	rm -f icetime$(EXE) icetime.exe *.o *.d timings-*.cc
 	rm -rf test[0-9]*
 
 -include *.d