# Lair Configuration DSL Case Study (Python 3)
# External DSL Parser using Parsita (builderParsita1.py)
# H. Conrad Cunningham

# Developed for CSci 658, Software Language Engineering, Spring 2018

#234567890123456789012345678901234567890123456789012345678901234567890

# 2018-03-05: (V1) Based partly on the 2013 builderLPEG.lua design 
# 2018-03-07: Small changes to comments

# This program uses the Python 3 parsing combinator package Parsita to
# parse the following grammar for an external Lair configuration DSL,
# to capture significant strings during parsing, and to generate the
# corresponding Lair semantic model objects.

# Config    ::=  Item^*
# Item      ::=  "item" Name Attribute^*
# Attribute ::=  Uses | Provides | Depends 
# Uses      ::=  "uses" Resource
# Provides  ::=  "provides" Resource
# Depends   ::=  "requires" Name
# Resource  ::=  Name [ PropList ]
# PropList  ::=  "{" Assign { "," Assign } "}" 
# Assign    ::=  Name "=" ResVal
# ResVal    ::=  Name | IntNum
# Name      ::=  identifier 
# IntNum    ::=  optional +/- followed by one or more digits

# Limitation: Values can only be integers and identifiers.
# TODO:       Expand this to include quoted strings!

# The same configuration used in the other DSL examples can be
# expressed  as follows:

# item secure_air_vent
# 
# item acid_bath
#     uses acid {type = hcl, grade = 5}
#     uses electricity {power = 12}
#  
# item camera
#     uses electricity{power = 1 }
# 
# item small_power_plant
#     provides electricity {power = 11}
#     requires secure_air_vent

# Import the parser combinator library and semantic model

from parsita import *
from model   import *

# Parsita TextParser
class LairParsers(TextParsers):

    # TODO: Reexamine how curconfig variable is written, accessed
    # Context variable (and symbol table) for configuration
    curconfig = None

    def return_config(t): # ignore argument
        return LairParsers.curconfig
    
    # Function "make_resource" implements a semantic action to create
    # a new Resource instance in the semantic model. It takes a
    # parse subtree "t" for a resource specification and produces the
    # corresponding semantic model object.

    # t = [name, [] ] if no properties
    # t = [name, [ [ (propname,propval) ... ] ] if one or more

    def make_resource(t):
        resource = None
        name = t[0]      # resource name: "acid" or "electricity"
        optprops = t[1]  # 0 or 1 property lists

        if name == "acid":
            resource = Acid()
            # properties type and grade supported
            if len(optprops) == 1:
                props = optprops[0] # property list of pairs
                for pair in props:  # for each pair
                    propname = pair[0]
                    propval  = pair[1] 
                    if propname == "type":
                        resource.type = propval
                    elif propname == "grade":
                        resource.grade = propval
                    else:
                        print(f'Acid property \"{propname}\" unknown')

        elif name == "electricity":
            # property power required, none others allowed
            if len(optprops) == 1 and len(optprops[0]) == 1:
                props    = optprops[0] # property list of pairs
                propname = props[0][0] # 1st component only pair
                propval  = props[0][1] # 2nd component only pair
                if propname == "power":
                    resource = Electricity(int(propval))
                else:
                    print('Electricity must have "power" property')
            else:
                print('Electrity has unknown properties')

        else:
            print(f'Resource type "{name}" not supported')

        return resource


    # Function "make_item" implements a semantic action to create a
    # new Item instance in the semantic model. It takes an item
    # specification and produces the corresponding semantic model Item
    # object and adding it to the Configuration object. (If the
    # configuration object does not exist, the function first creates
    # it.)
    # t = ['item', name, [] ] if no attributes
    # t = ['item', name, [ (type,val) ... ] if 1 or more

    def make_item(t):
        if t[0] != "item":
            print("Error: invalid attempt to make item: " + str(t))
            return None
        if LairParsers.curconfig == None:
            LairParsers.curconfig = Configuration()
        name = t[1]      # item name
        attrlist = t[2]  # list of attributes
        curitem = Item(name)
        if len(attrlist) > 0:
            for attr in attrlist:
                typ = attr[0]
                val = attr[1]
                if typ == "uses":
                    curitem.add_usage(val)
                elif typ == "provides":
                    curitem.add_provision(val)
                elif typ == "requires":  # Note 1 below
                    curitem.add_dependency(
                        LairParsers.curconfig.get_item(val))
                else:
                    print(f'Unknown attribute type \"{typ}\"' +
                        f'for item \"{name}\"')
        LairParsers.curconfig.add_item(curitem)
        return curitem

    # Note 1: This approach ensures that any other items depended upon
    # by the current item has been defined previously, using the
    # "curconfig" context variable. (That is, there are no forward
    # references.) The config rule does not construct the
    # configuration from the items; it just returns "curconfig" upon a
    # successful parse. An alternative would be to have a second pass
    # triggered by the config rule that check/ssets the forward
    # references.

    # The transformation to tuples is not strictly necessary, but this
    # did help me to visually see the pairs of values in the parse
    # tree "lists of lists".

    # Grammar rules using Parsita (written bottom up)
    name      = reg(r'[_A-Za-z][_A-Za-z0-9]*')
    intnum    = reg(r'[+-]?\d+')  > int
    resval    = intnum | name
    assign    = name & ('=' >> resval)  > tuple
    proplist  = '{' >> rep1sep(assign, ',') << '}'
    resource  = (name & opt(proplist))  > make_resource
    uses      = ('uses' & resource)     > tuple
    provides  = ('provides' & resource) > tuple
    depends   = ('requires' & name)     > tuple
    attribute = uses | provides | depends
    item      = ('item' & name & rep(attribute)) > make_item
    config    = rep(item)               > return_config # Note 1

    # Read and parse a DSL input file
    def load_dsl(dsl_file):
        with open(dsl_file, 'r') as f:
            dsl_str = f.read()
        result = LairParsers.config.parse(dsl_str)
        return result

# Miscellaneous testing code used in development
if __name__ == '__main__':
    print("name:     " +
        str(LairParsers.name.parse("conrad_12").value))
    print("intnum:   " +
        str(LairParsers.intnum.parse("12").value))
    print("resval:   " +
        str(LairParsers.resval.parse("12").value))
    print("resval:   " +
        str(LairParsers.resval.parse("conrad13").value))
    print("assign:   " +
        str(LairParsers.assign.parse("type = hcl").value))
    print("proplist:  " +
        str(LairParsers.proplist.parse(
            "{ type = hcl, grade = 5 }").value))
    print("resource: " +
        str(LairParsers.resource.parse(
            "acid { type = hcl, grade = 5 }").value))
    print("resource: " +
        str(LairParsers.resource.parse("acid ").value))
    print("resource: " +
        str(LairParsers.resource.parse(
            "electricity{power=11}").value))
    print("uses:     " +
        str(LairParsers.uses.parse(
            "uses acid { type = hcl, grade = 5 }").value))
    print("provides: " +
        str(LairParsers.provides.parse(
            "provides electricity {power = 11}").value))
    print("item:     " +
        str(LairParsers.item.parse("item secure_air_vent").value))
    print("item:     " +
        str(LairParsers.item.parse('''\
            item acid_bath
                uses acid {type = hcl, grade = 5}
                uses electricity {power = 12}
            ''').value))

    dsl = '''\
item secure_air_vent

item acid_bath
    uses acid {type = hcl, grade = 5}
    uses electricity {power = 12}

item camera
    uses electricity{power = 1 }

item small_power_plant
    provides electricity {power = 11}
    requires secure_air_vent'''
    
    print("Full dsl test:\n" + dsl)
    result = LairParsers.config.parse(dsl)
    print("Config:   " + str(result.value))
