fasm2frame: optional value on simple elements

Remove INT prefixes not in segdb
Better test cases

Signed-off-by: John McMaster <johndmcmaster@gmail.com>
diff --git a/tools/fasm2frame.py b/tools/fasm2frame.py
index b288f31..10baa80 100755
--- a/tools/fasm2frame.py
+++ b/tools/fasm2frame.py
@@ -4,9 +4,37 @@
 import re
 import sys
 import json
+import collections
 
-# Based on segprint function
-# Modified to return dict instead of list
+
+class FASMSyntaxError(Exception):
+    pass
+
+
+def parsebit(val):
+    '''Return "!012_23" => (12, 23, False)'''
+    isset = True
+    # Default is 0. Skip explicit call outs
+    if val[0] == '!':
+        isset = False
+        val = val[1:]
+    # 28_05 => 28, 05
+    seg_word_column, word_bit_n = val.split('_')
+    return int(seg_word_column), int(word_bit_n), isset
+
+
+'''
+Loosely based on segprint function
+Maybe better to return as two distinct dictionaries?
+
+{
+    'tile.blah': {None: (12, 23)},
+    'tile.meh': {
+            'O5': [(11, 2, False), (12, 2, True)],
+            'O6': [(11, 2, True), (12, 2, False)],
+    },
+}
+'''
 segbitsdb = dict()
 
 
@@ -16,25 +44,49 @@
 
     segbitsdb[segtype] = {}
 
+    def process(l):
+        l = l.strip()
+
+        # CLBLM_L.SLICEL_X1.ALUT.INIT[10] 29_14
+        parts = line.split()
+        name = parts[0]
+        bit_vals = parts[1:]
+
+        # Assumption
+        # only 1 bit => non-enumerated value
+        if len(bit_vals) == 1:
+            seg_word_column, word_bit_n, isset = parsebit(bit_vals[0])
+            if not isset:
+                raise Exception(
+                    "Expect single bit DB entries to be set, got %s" % l)
+            segbitsdb[segtype][name] = {None: (seg_word_column, word_bit_n)}
+        else:
+            # An enumerated value
+            # Split the base name and selected key
+            m = re.match(r'(.+)[.](.+)', name)
+            name = m.group(1)
+            key = m.group(2)
+            # May or may not be the first key encountered
+            m = segbitsdb[segtype].get(name, {})
+            segbitsdb[segtype][name] = m
+            l = []
+            m[key] = l
+            #print("Add %s.%s" % (name, key))
+            for bit_val in bit_vals:
+                seg_word_column, word_bit_n, isset = parsebit(bit_val)
+                l.append((seg_word_column, word_bit_n, isset))
+
     with open("%s/%s/segbits_%s.db" % (os.getenv("XRAY_DATABASE_DIR"),
                                        os.getenv("XRAY_DATABASE"), segtype),
               "r") as f:
         for line in f:
-            # CLBLM_L.SLICEL_X1.ALUT.INIT[10] 29_14
-            parts = line.split()
-            name = parts[0]
-            vals = parts[1:]
-            segbitsdb[segtype][name] = vals
+            process(line)
 
     with open("%s/%s/segbits_int_%s.db" %
               (os.getenv("XRAY_DATABASE_DIR"), os.getenv("XRAY_DATABASE"),
                segtype[-1]), "r") as f:
         for line in f:
-            # CLBLM_L.SLICEL_X1.ALUT.INIT[10] 29_14
-            parts = line.split()
-            name = parts[0]
-            vals = parts[1:]
-            segbitsdb[segtype][name] = vals
+            process(line)
 
     return segbitsdb[segtype]
 
@@ -79,6 +131,10 @@
 def run(f_in, f_out, sparse=False, debug=False):
     # address to array of 101 32 bit words
     frames = {}
+    # Directives we've seen so far
+    # Complain if there is a duplicate
+    # Contains line number of last entry
+    used_names = {}
 
     def frames_init():
         '''Set all frames to 0'''
@@ -97,6 +153,10 @@
         '''Set given bit in given frame address and word'''
         frames[frame_addr][word_addr] |= 1 << bit_index
 
+    def frame_clear(frame_addr, word_addr, bit_index):
+        '''Set given bit in given frame address and word'''
+        frames[frame_addr][word_addr] &= 0xFFFFFFFF ^ (1 << bit_index)
+
     with open("%s/%s/tilegrid.json" % (os.getenv("XRAY_DATABASE_DIR"),
                                        os.getenv("XRAY_DATABASE")), "r") as f:
         grid = json.load(f)
@@ -105,7 +165,7 @@
         # Initiaize bitstream to 0
         frames_init()
 
-    for l in f_in:
+    for line_number, l in enumerate(f_in, 1):
         # Comment
         # Remove all text including and after #
         i = l.rfind('#')
@@ -119,16 +179,22 @@
 
         # tile.site.stuff value
         # INT_L_X10Y102.CENTER_INTER_L.IMUX_L1 EE2END0
-        m = re.match(
-            r'([a-zA-Z0-9_]+)[.]([a-zA-Z0-9_]+)[.]([a-zA-Z0-9_.\[\]]+)[ ](.+)',
-            l)
+        # Optional value
+        m = re.match(r'([a-zA-Z0-9_]+)[.]([a-zA-Z0-9_.\[\]]+)([ ](.+))?', l)
         if not m:
-            raise Exception("Bad line: %s" % l)
+            raise FASMSyntaxError("Bad line: %s" % l)
         tile = m.group(1)
-        site = m.group(2)
-        suffix = m.group(3)
+        name = m.group(2)
         value = m.group(4)
 
+        used_name = (tile, name)
+        old_line_number = used_names.get(used_name, None)
+        if old_line_number:
+            raise FASMSyntaxError(
+                "Duplicate name lines %d and %d, second line: %s" %
+                (old_line_number, line_number, l))
+        used_names[used_name] = line_number
+
         tilej = grid['tiles'][tile]
         seg = tilej['segment']
         segj = grid['segments'][seg]
@@ -140,53 +206,60 @@
         for coli in range(segj['frames']):
             frame_init(seg_baseaddr + coli)
 
-        # Now lets look up the bits we need frames for
-        segdb = get_database(segj['type'])
-
-        def clb2dbkey(tile, tilej, site, suffix, value):
-            db_k = '%s.%s.%s' % (tilej['type'], site, suffix)
-            return db_k
-
-        def int2dbkey(tile, tilej, site, suffix, value):
-            return '%s.%s.%s' % (tilej['type'], suffix, value)
-
-        tile2dbkey = {
-            'CLBLL_L': clb2dbkey,
-            'CLBLL_R': clb2dbkey,
-            'CLBLM_L': clb2dbkey,
-            'CLBLM_R': clb2dbkey,
-            'INT_L': int2dbkey,
-            'INT_R': int2dbkey,
-            'HCLK_L': int2dbkey,
-            'HCLK_R': int2dbkey,
-        }
-
-        f = tile2dbkey.get(tilej['type'], None)
-        if f is None:
-            raise Exception("Unhandled segment type %s" % tilej['type'])
-        db_k = f(tile, tilej, site, suffix, value)
-
-        try:
-            db_vals = segdb[db_k]
-        except KeyError:
-            raise Exception(
-                "Key %s (from line '%s') not found in segment DB %s" %
-                (db_k, l, segj['type']))
-
-        for val in db_vals:
-            # Default is 0. Skip explicit call outs
-            if val[0] == '!':
-                continue
-            # 28_05 => 28, 05
-            seg_word_column, word_bit_n = val.split('_')
-            seg_word_column, word_bit_n = int(seg_word_column), int(word_bit_n)
+        def update_segbit(seg_word_column, word_bit_n, isset):
+            '''Set  or clear a single bit in a segment at the given word column and word bit position'''
             # Now we have the word column and word bit index
             # Combine with the segments relative frame position to fully get the position
             frame_addr = seg_baseaddr + seg_word_column
             # 2 words per segment
             word_addr = seg_word_base + word_bit_n // 32
             bit_index = word_bit_n % 32
-            frame_set(frame_addr, word_addr, bit_index)
+            if isset:
+                frame_set(frame_addr, word_addr, bit_index)
+            else:
+                frame_clear(frame_addr, word_addr, bit_index)
+
+        # Now lets look up the bits we need frames for
+        segdb = get_database(segj['type'])
+
+        db_k = '%s.%s' % (tilej['type'], name)
+        try:
+            db_vals = segdb[db_k]
+        except KeyError:
+            raise FASMSyntaxError(
+                "Segment DB %s, key %s not found from line '%s'" %
+                (segj['type'], db_k, l))
+        '''
+        Creating the key depends on whether its an enumerated or a fixed value
+        If its enumerated, the value forms part of the key
+        We'd also like to ideally identify if there is a syntax error
+        Ultimately probably easier to separate out these two cases
+        '''
+        # A single bit value?
+        if None in db_vals:
+            seg_word_column, word_bit_n = db_vals[None]
+            # Value optional, defaults to 1
+            isset = 1
+            if value:
+                isset = {'0': 0, '1': 1}.get(value, None)
+                if isset is None:
+                    raise FASMSyntaxError(
+                        "Bad binary value %s on line %s" % (value, l))
+            update_segbit(seg_word_column, word_bit_n, isset)
+        # An enumerated value
+        else:
+            if not value:
+                raise FASMSyntaxError(
+                    "Enumerable entry %s must have explicit value" % name)
+            # Get the specific entry we need
+            try:
+                db_vals = db_vals[value]
+            except KeyError:
+                raise FASMSyntaxError(
+                    "Invalid entry %s. Valid entries are %s" %
+                    (value, db_vals.keys()))
+            for seg_word_column, word_bit_n, isset in db_vals:
+                update_segbit(seg_word_column, word_bit_n, isset)
 
     if debug:
         #dump_frames_verbose(frames)
diff --git a/tools/segprint2fasm.py b/tools/segprint2fasm.py
index fd185fb..5906044 100755
--- a/tools/segprint2fasm.py
+++ b/tools/segprint2fasm.py
@@ -5,29 +5,50 @@
 import sys
 import json
 
+enumdb = dict()
+
+
+def get_enums(segtype):
+    if segtype in enumdb:
+        return enumdb[segtype]
+
+    enumdb[segtype] = {}
+
+    def process(l):
+        l = l.strip()
+
+        # CLBLM_L.SLICEL_X1.ALUT.INIT[10] 29_14
+        parts = line.split()
+        name = parts[0]
+        bit_vals = parts[1:]
+
+        # Assumption
+        # only 1 bit => non-enumerated value
+        enumdb[segtype][name] = len(bit_vals) != 1
+
+    with open("%s/%s/segbits_%s.db" % (os.getenv("XRAY_DATABASE_DIR"),
+                                       os.getenv("XRAY_DATABASE"), segtype),
+              "r") as f:
+        for line in f:
+            process(line)
+
+    with open("%s/%s/segbits_int_%s.db" %
+              (os.getenv("XRAY_DATABASE_DIR"), os.getenv("XRAY_DATABASE"),
+               segtype[-1]), "r") as f:
+        for line in f:
+            process(line)
+
+    return enumdb[segtype]
+
+
+def isenum(segtype, tag):
+    return get_enums(segtype)[tag]
+
 
 def tag2fasm(grid, seg, tag):
     '''Given tilegrid, segment name and tag, return fasm directive'''
     segj = grid['segments'][seg]
 
-    def clbf(seg, tile, tag_post):
-        return '%s.%s 1' % (tile, tag_post)
-
-    def intf(seg, tile, tag_post):
-        # Make the selection an argument of the configruation
-        m = re.match(r'(.*)[.]([A-Za-z0-9_]+)', tag_post)
-        which = m.group(1)
-        value = m.group(2)
-        site = {
-            'clbll_l': 'CENTER_INTER_L',
-            'clbll_r': 'CENTER_INTER_R',
-            'clblm_l': 'CENTER_INTER_L',
-            'clblm_r': 'CENTER_INTER_R',
-            'hclk_l': 'HCLK_L',
-            'hclk_r': 'HCLK_R',
-        }[segj['type']]
-        return '%s.%s.%s %s' % (tile, site, which, value)
-
     m = re.match(r'([A-Za-z0-9_]+)[.](.*)', tag)
     tile_type = m.group(1)
     tag_post = m.group(2)
@@ -39,20 +60,14 @@
     else:
         raise Exception("Couldn't find tile type %s" % tile_type)
 
-    tag2asm = {
-        'CLBLL_L': clbf,
-        'CLBLL_R': clbf,
-        'CLBLM_L': clbf,
-        'CLBLM_R': clbf,
-        'INT_L': intf,
-        'INT_R': intf,
-        'HCLK_L': intf,
-        'HCLK_R': intf,
-    }
-    f = tag2asm.get(tile_type, None)
-    if f is None:
-        raise Exception("Unhandled segment type %s" % tile_type)
-    return f(seg, tile, tag_post)
+    if not isenum(segj['type'], tag):
+        return '%s.%s 1' % (tile, tag_post)
+    else:
+        # Make the selection an argument of the configruation
+        m = re.match(r'(.*)[.]([A-Za-z0-9_]+)', tag_post)
+        which = m.group(1)
+        value = m.group(2)
+        return '%s.%s %s' % (tile, which, value)
 
 
 def run(f_in, f_out, sparse=False):
diff --git a/tools/test_data/ff_int.fasm b/tools/test_data/ff_int.fasm
index 3d7e30b..4406ed6 100644
--- a/tools/test_data/ff_int.fasm
+++ b/tools/test_data/ff_int.fasm
@@ -2,23 +2,22 @@
 # segprint -zd test_data/clb_ff/design.bits
 
 # FF as LDCE
-CLBLM_L_X10Y102.SLICEM_X0.AFF.DMUX.AX 1
+CLBLM_L_X10Y102.SLICEM_X0.AFF.DMUX AX
 CLBLM_L_X10Y102.SLICEM_X0.AFF.ZINI 1
 CLBLM_L_X10Y102.SLICEM_X0.AFF.ZRST 1
 CLBLM_L_X10Y102.SLICEM_X0.CEUSEDMUX 1
+# CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX 1
 CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX 1
-# CLBLM_L_X10Y102.SLICEM_X0.FFSYNC 0
-# CLBLM_L_X10Y102.SLICEM_X0.LATCH 0
 
 # Note: a number of pseudo pips here
 # Omitted
-INT_L_X10Y102.CENTER_INTER_L.BYP_ALT0 EE2END0
-INT_L_X10Y102.CENTER_INTER_L.BYP_ALT1 EL1END1
-INT_L_X10Y102.CENTER_INTER_L.CLK_L1 GCLK_L_B11_WEST
-INT_L_X10Y102.CENTER_INTER_L.CTRL_L1 ER1END2
-INT_L_X10Y102.CENTER_INTER_L.FAN_ALT7 BYP_BOUNCE0
-INT_L_X10Y102.CENTER_INTER_L.WW2BEG0 LOGIC_OUTS_L4
+INT_L_X10Y102.BYP_ALT0 EE2END0
+INT_L_X10Y102.BYP_ALT1 EL1END1
+INT_L_X10Y102.CLK_L1 GCLK_L_B11_WEST
+INT_L_X10Y102.CTRL_L1 ER1END2
+INT_L_X10Y102.FAN_ALT7 BYP_BOUNCE0
+INT_L_X10Y102.WW2BEG0 LOGIC_OUTS_L4
 
-HCLK_L_X31Y130.HCLK_L.ENABLE_BUFFER HCLK_CK_BUFHCLK8
-HCLK_L_X31Y130.HCLK_L.HCLK_LEAF_CLK_B_BOTL5 HCLK_CK_BUFHCLK8
+HCLK_L_X31Y130.ENABLE_BUFFER.HCLK_CK_BUFHCLK8 1
+HCLK_L_X31Y130.HCLK_LEAF_CLK_B_BOTL5 HCLK_CK_BUFHCLK8
 
diff --git a/tools/test_data/ff_int_0s.fasm b/tools/test_data/ff_int_0s.fasm
new file mode 100644
index 0000000..3f113f7
--- /dev/null
+++ b/tools/test_data/ff_int_0s.fasm
@@ -0,0 +1,25 @@
+# Loosely based on
+# segprint -zd test_data/clb_ff/design.bits
+
+# FF as LDCE
+CLBLM_L_X10Y102.SLICEM_X0.AFF.DMUX AX
+CLBLM_L_X10Y102.SLICEM_X0.AFF.ZINI 1
+CLBLM_L_X10Y102.SLICEM_X0.AFF.ZRST 1
+CLBLM_L_X10Y102.SLICEM_X0.CEUSEDMUX 1
+CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX 1
+# Unused bits explicitly set to 0
+CLBLM_L_X10Y102.SLICEM_X0.FFSYNC 0
+CLBLM_L_X10Y102.SLICEM_X0.LATCH 0
+
+# Note: a number of pseudo pips here
+# Omitted
+INT_L_X10Y102.BYP_ALT0 EE2END0
+INT_L_X10Y102.BYP_ALT1 EL1END1
+INT_L_X10Y102.CLK_L1 GCLK_L_B11_WEST
+INT_L_X10Y102.CTRL_L1 ER1END2
+INT_L_X10Y102.FAN_ALT7 BYP_BOUNCE0
+INT_L_X10Y102.WW2BEG0 LOGIC_OUTS_L4
+
+HCLK_L_X31Y130.ENABLE_BUFFER.HCLK_CK_BUFHCLK8 1
+HCLK_L_X31Y130.HCLK_LEAF_CLK_B_BOTL5 HCLK_CK_BUFHCLK8
+
diff --git a/tools/test_data/ff_int_op1.fasm b/tools/test_data/ff_int_op1.fasm
new file mode 100644
index 0000000..c77b5ff
--- /dev/null
+++ b/tools/test_data/ff_int_op1.fasm
@@ -0,0 +1,24 @@
+# Loosely based on
+# segprint -zd test_data/clb_ff/design.bits
+
+# FF as LDCE
+CLBLM_L_X10Y102.SLICEM_X0.AFF.DMUX AX
+CLBLM_L_X10Y102.SLICEM_X0.AFF.ZINI 1
+CLBLM_L_X10Y102.SLICEM_X0.AFF.ZRST 1
+CLBLM_L_X10Y102.SLICEM_X0.CEUSEDMUX 1
+# CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX 1
+# Optional entry
+CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX
+
+# Note: a number of pseudo pips here
+# Omitted
+INT_L_X10Y102.BYP_ALT0 EE2END0
+INT_L_X10Y102.BYP_ALT1 EL1END1
+INT_L_X10Y102.CLK_L1 GCLK_L_B11_WEST
+INT_L_X10Y102.CTRL_L1 ER1END2
+INT_L_X10Y102.FAN_ALT7 BYP_BOUNCE0
+INT_L_X10Y102.WW2BEG0 LOGIC_OUTS_L4
+
+HCLK_L_X31Y130.ENABLE_BUFFER.HCLK_CK_BUFHCLK8 1
+HCLK_L_X31Y130.HCLK_LEAF_CLK_B_BOTL5 HCLK_CK_BUFHCLK8
+
diff --git a/tools/test_data/lut_int.fasm b/tools/test_data/lut_int.fasm
index df88dd5..0fca202 100644
--- a/tools/test_data/lut_int.fasm
+++ b/tools/test_data/lut_int.fasm
@@ -18,18 +18,18 @@
 
 # din bus
 # din[0]
-INT_L_X10Y102.CENTER_INTER_L.IMUX_L1 EE2END0
+INT_L_X10Y102.IMUX_L1 EE2END0
 # din[1]
-INT_L_X10Y102.CENTER_INTER_L.IMUX_L2 EE2END1
+INT_L_X10Y102.IMUX_L2 EE2END1
 # din[2]
-INT_L_X10Y102.CENTER_INTER_L.IMUX_L4 EE2END2
+INT_L_X10Y102.IMUX_L4 EE2END2
 # din[3]
-INT_L_X10Y102.CENTER_INTER_L.IMUX_L7 EE2END3
+INT_L_X10Y102.IMUX_L7 EE2END3
 # din[4]
-INT_L_X10Y102.CENTER_INTER_L.IMUX_L8 EL1END0
+INT_L_X10Y102.IMUX_L8 EL1END0
 # din[5]
-INT_L_X10Y102.CENTER_INTER_L.IMUX_L11 EL1END1
+INT_L_X10Y102.IMUX_L11 EL1END1
 
 # dout[0]
-INT_L_X10Y102.CENTER_INTER_L.WW2BEG0 LOGIC_OUTS_L12
+INT_L_X10Y102.WW2BEG0 LOGIC_OUTS_L12
 
diff --git a/tools/test_fasm2frame.py b/tools/test_fasm2frame.py
index 87f372f..27c2815 100644
--- a/tools/test_fasm2frame.py
+++ b/tools/test_fasm2frame.py
@@ -1,3 +1,5 @@
+# TODO: need better coverage for different tile types
+
 import fasm2frame
 
 import unittest
@@ -74,6 +76,63 @@
         self.bitread_frm_equals(
             'test_data/ff_int.fasm', 'test_data/ff_int/design.bits')
 
+    def test_ff_int_op1(self):
+        '''Omitted key set to '''
+        self.bitread_frm_equals(
+            'test_data/ff_int_op1.fasm', 'test_data/ff_int/design.bits')
+
+    # Same check as above, but isolated test case
+    def test_opkey_01_default(self):
+        '''Optional key with binary omitted value should produce valid result'''
+        fin = StringIO.StringIO("CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX")
+        fout = StringIO.StringIO()
+        fasm2frame.run(fin, fout)
+
+    def test_opkey_01_1(self):
+        fin = StringIO.StringIO("CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX 1")
+        fout = StringIO.StringIO()
+        fasm2frame.run(fin, fout)
+
+    def test_opkey_enum(self):
+        '''Optional key with enumerated value should produce syntax error'''
+        # CLBLM_L.SLICEM_X0.AMUX.O6 !30_06 !30_07 !30_08 30_11
+        fin = StringIO.StringIO("CLBLM_L_X10Y102.SLICEM_X0.AMUX.O6")
+        fout = StringIO.StringIO()
+        try:
+            fasm2frame.run(fin, fout)
+            self.fail("Expected syntax error")
+        except fasm2frame.FASMSyntaxError:
+            pass
+
+    def test_ff_int_0s(self):
+        '''Explicit 0 entries'''
+        self.bitread_frm_equals(
+            'test_data/ff_int_0s.fasm', 'test_data/ff_int/design.bits')
+
+    def test_badkey(self):
+        '''Bad key should throw syntax error'''
+        fin = StringIO.StringIO("CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX 2")
+        fout = StringIO.StringIO()
+        try:
+            fasm2frame.run(fin, fout)
+            self.fail("Expected syntax error")
+        except fasm2frame.FASMSyntaxError:
+            pass
+
+    def test_dupkey(self):
+        '''Duplicate key should throw syntax error'''
+        fin = StringIO.StringIO(
+            """\
+CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX 0
+CLBLM_L_X10Y102.SLICEM_X0.SRUSEDMUX 1
+""")
+        fout = StringIO.StringIO()
+        try:
+            fasm2frame.run(fin, fout)
+            self.fail("Expected syntax error")
+        except fasm2frame.FASMSyntaxError:
+            pass
+
     def test_sparse(self):
         '''Verify sparse equivilent to normal encoding'''
         frm_fn = 'test_data/lut_int.fasm'