#!/usr/bin/env python

################################################################################
##{{{ Copyright 2010 C. D. Stahl, All rights reserved.
##
##    1. Redistributions of source code must retain the above copyright 
##       notice, this list of conditions and the following disclaimer.
##    
##    2. Redistributions in binary form must reproduce the above copyright 
##       notice, this list of conditions and the following disclaimer in 
##       the documentation and/or other materials provided with the 
##       distribution.
##    
##    THIS SOFTWARE IS PROVIDED BY C. D. STAHL ``AS IS'' AND ANY EXPRESS OR 
##    IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
##    OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
##    IN NO EVENT SHALL C. D. STAHL OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
##    INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
##    (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
##    SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
##    CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
##    LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
##    OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
##    SUCH DAMAGE.
## 
##}}}


################################################################################
##{{{ Notes and Import
# This is a simple parser.  it's not perfect by a long shot.  The
# goal is to blend python and verilog, but not interfere with valid
# verilog -- running a valid verilog file through the parser will
# result in the valid verilog file passing unmodified.
#
# There are a lot of corner cases, because both python and verilog
# use the same symbols, sometimes differently.  These cases don't
# come up often.
# 
# 
# the main assumptions I have are:
# 1. eval will have matched {}'s only.
#    eg  x <= { ${"}"};
#    evals to x <= { "};
# 2. exec will have matched []'s only.
#    eg: $[ x = re.compile(r'\:[)}\]]')]
#    evals to x = re.compile(r'\:[)}\]]') in this case.
# 3. block will not contain ). within.
#    eg: $(param L = 0 // length (always even).).if(use_len)
#    also evaluates correctly, but looks bad.
# 4. the user doesn't use """ within the verilog code (or comments)
#
# the parser can pick
#
################################################################################
import sys, re, time
##}}}


################################################################################
##{{{ find valid closing marker
# returns a location
def close(text, sre, ere):
  loc = 0 
  disp = 1
  while disp:
    s = sre(text[loc:])
    e = ere(text[loc:])
    if s and e:
      if s.start() < e.start():
        disp += 1
        loc += s.end()
      else:
        disp -= 1
        loc += e.end()
    elif e:
      disp -= 1
      loc += e.end()
    else: # no need to look for an unmatched start
      disp = 0
      loc = len(text)
  return loc
##}}}


################################################################################
##{{{ find first match that is not None
# find the first match that is a match
# locs is a list of lists
def first_found(locs):
  found = None
  floc = None
  for n,loc in enumerate(locs):
    if loc is not None:
      if found is not None:
        if loc[0] < found:
          found = loc[0]
          floc = n
      else:
        found = loc[0]
        floc = n
  return floc
##}}}


################################################################################
##{{{ Regular Expressions
eval_sre = re.compile(r'\$\{', re.MULTILINE).search
exec_sre = re.compile(r'\$\[', re.MULTILINE).search

curly_sre  = re.compile(r'(?<!\\)\{', re.MULTILINE).search
curly_ere  = re.compile(r'(?<!\\)\}', re.MULTILINE).search
paren_sre  = re.compile(r'(?<!\\)\(', re.MULTILINE).search
paren_ere  = re.compile(r'(?<!\\)\)', re.MULTILINE).search
square_sre = re.compile(r'(?<!\\)\[', re.MULTILINE).search
square_ere = re.compile(r'(?<!\\)\]', re.MULTILINE).search

block_sre = re.compile(r'\$\(', re.MULTILINE).search
block_ere = re.compile(r'\)\.(?=[a-z])', re.MULTILINE).search
cmd_sre   = re.compile(r'([a-z]+\()', re.MULTILINE).search

curly_grp  = lambda text: close(text,curly_sre,curly_ere);
paren_grp  = lambda text: close(text,paren_sre,paren_ere);
square_grp = lambda text: close(text,square_sre,square_ere);
block_grp  = lambda text: close(text,block_sre,block_ere);
dq_grp     = lambda text: close(text,dq_re,dq_re);
##}}}


################################################################################
##{{{ eval
def find_eval(text):
  s = eval_sre(text)
  if s:
    e = curly_grp(text[s.end():])
    return [s.end(), s.end()+e]
  return None
##}}}


################################################################################
##{{{ exec
def find_exec(text):
  s = exec_sre(text)
  if s:
    e = square_grp(text[s.end():])
    return [s.end(), s.end()+e]
  return None
##}}}


################################################################################
##{{{ block
def find_block(text):
  s = block_sre(text)
  if s:
    e = block_grp(text[s.end():])
    cmd  = cmd_sre(text[s.end()+e:])
    if cmd:
      alen = paren_grp(text[s.end()+e+cmd.end():])
      tmp = [s.end()]
      tmp.append(tmp[-1]+e)
      tmp.append(tmp[-1]+cmd.end())
      tmp.append(tmp[-1]+alen)
      return tmp
  return None
##}}}


################################################################################
##{{{ Recursive Elaboration
def rmake(f, idt):
  loc = 0
  end = len(f)
  ret = ""
  while loc < end:
    # find next starting token
    seval  = find_eval(f[loc:])
    sexec  = find_exec(f[loc:])
    sblock = find_block(f[loc:])
    next_obj = first_found([seval, sexec, sblock])
  
    # find expected ending token
    # ending case
    if next_obj is None:
      ret += idt*"  "+'hdl+="""%s"""\n'%(f[loc:])
      loc = end
  
    # eval case
    elif next_obj == 0:
      if seval[0] > 0:
        ret += idt*"  "+'hdl+="""%s"""\n'%(f[loc:loc+seval[0]-2])
      ret += idt*"  "+"hdl+=str(%s)\n"%(f[loc+seval[0]:loc+seval[1]-1])
      loc += seval[-1]
        
    # exec case (with alignment)
    elif next_obj == 1:
      if sexec[0] > 0:
        ret += idt*"  "+'hdl+="""%s"""\n'%(f[loc:loc+sexec[0]-2])
      lines= f[loc+sexec[0]:loc+sexec[1]-1].split('\n')
      if len(lines) > 1:
        ws = re.compile(r'^(\s*)').match(lines[1]).group(1)
        ret += idt*"  "+ lines[0]+'\n'
        for line in lines[1:]:
          lre = re.compile("^%s(.*)"%(ws)).search(line)
          if lre:
            ret += idt*"  " + lre.group(1)+"\n"
          else: 
            ret += idt*"  " + line + "\n"
      else:
        ret += idt*"  " + lines[0] + "\n"
      loc += sexec[-1]
  
    # block case
    elif next_obj == 2:
      if sblock[0] > 0:
        ret += idt*"  "+'hdl+="""%s"""\n'%(f[loc:loc+sblock[0]-2])
      btype = f[loc+sblock[1]:loc+sblock[2]-1]
      barg = f[loc+sblock[2]:loc+sblock[3]-1]
      if btype == "if":
        ret += idt*"  "+"if %s:\n"%(barg)
        ret += rmake(f[loc+sblock[0]:loc+sblock[1]-2],idt+1)
      elif btype == "clone":
        ret += idt*"  "+"for %s:\n"%(barg)
        ret += rmake( f[loc+sblock[0]:loc+sblock[1]-2],idt+1)
      elif btype == "join":
        bargs = re.compile(r'^\s*?(?<!\\)\"(.*?)(?<!\\)\"\s*?,(.*)', re.MULTILINE).search(barg)
        ret += idt*"  "+"for %s:\n"%(bargs.group(2))
        ret += rmake( f[loc+sblock[0]:loc+sblock[1]-2],idt+1)
        ret += (idt+1)*"  "+'hdl+="""%s"""\n'%(bargs.group(1))
        ret += idt*"  "+"hdl = hdl[:-%d]\n"%(len(bargs.group(1)))
      loc += sblock[-1]
  return ret
##}}}


################################################################################
##{{{ Main
if __name__ == '__main__':
  import sys
  from optparse import OptionParser
  
  # setup options
  parser = OptionParser()
  parser.add_option("-i", "--inplace", help="overwrite source files",
                    action="store_true", dest="inplace", default=False)
  parser.add_option("-g", "--genonly", help="only make code generator",
                    action="store_true", dest="gen", default=False)

  (options, args) = parser.parse_args()
  
  if len(args) == 0:
    isrc = [sys.stdin]
    osrc = [sys.stdout]
  else:
    isrc = args
    if options.inplace:
      osrc = args
    else:
      osrc = len(args)*[sys.stdout]

  # main loop
  for src in range(len(isrc)):
    if isrc[src] is not sys.stdin:
      fi = open(isrc[src])
    else:
      fi = sys.stdin

    cg = ""
    template = fi.read()
    fi.close()
    cg  = "hdl=''\n"
    cg += rmake(template, 0, 0)
    if options.gen:
      hdl = cg
    else:
      exec(cg)
    
    if osrc[src] is not sys.stdout:
      fo = open(osrc[src],"w")
    else:
      fo = sys.stdout

    fo.write(hdl)
    fo.close()

##}}}


