import json
import re
import sys

token_patterns = [
    (r'#[^\n]*', None),  
    (r'\s+', None), 
    (r'[', 'LBRACKET'),
    (r']', 'RBRACKET'),
    (r'{', 'LBRACE'),
    (r'}', 'RBRACE'),
    (r'%{', 'LMAPBRACE'),
    (r'=>', 'ARROW'),
    (r',', 'COMMA'),
    (r'(true|false)', 'BOOLEAN'),
    (r'(0|[1-9][0-9_]*)', 'INTEGER'),  
    (r':[A-Za-z_]\w*', 'ATOM'),  
    (r'[A-Za-z_]\w*', 'KEY'),  
]

regex_parts = [f'(?P<{name}>{pattern})' if name else f'(?:{pattern})' for pattern, name in token_patterns]
token_regex = '|'.join(regex_parts)
token_re = re.compile(token_regex)

class TokenizerException(Exception): pass


class ParserException(Exception): pass


def tokenize(text):
    pos = 0
    while pos < len(text):
        match = token_re.match(text, pos)
        if not match:
            raise TokenizerException(f'Illegal character {text[pos]!r} at index {pos}')
        pos = match.end()
        if match.lastgroup:  
            yield match.lastgroup, match.group(match.lastgroup)


class Node:
    def __init__(self, kind, value):
        self.kind = kind
        self.value = value

    def to_json(self):
        if self.kind == "map":
            return {"%k": self.kind, "%v": {k.to_json()["%v"]: v.to_json() for k, v in self.value}}
        elif self.kind == "list":
            return {"%k": self.kind, "%v": [item.to_json() for item in self.value]}
        else:
            return {"%k": self.kind, "%v": self.value}


class Parser:
    def __init__(self, tokens):
        self.tokens = iter(tokens)
        self.current_token = None
        self.next_token()

    def next_token(self):
        try:
            self.current_token = next(self.tokens)
        except StopIteration:
            self.current_token = None

    def parse(self):
        result = self.parse_sentence()
        if self.current_token is not None:
            raise ParserException('Unexpected token at the end')
        return result

    def parse_sentence(self):
        nodes = []
        while self.current_token is not None:
            node = self.parse_data_literal()
            nodes.append(node)
            if self.current_token and self.current_token[0] in {'COMMA', 'COLON', 'ARROW'}: 
               self.next_token()
        return nodes

    def parse_data_literal(self):
        if self.current_token[0] == 'LBRACKET':
            return self.parse_list()
        elif self.current_token[0] == 'LBRACE':
            return self.parse_tuple()
        elif self.current_token[0] == 'LMAPBRACE':
            return self.parse_map()
        elif self.current_token[0] == 'INTEGER':
            value = int(self.current_token[1])
            self.next_token()
            return Node('int', value)
        elif self.current_token[0] == 'ATOM':
            value = self.current_token[1]
            self.next_token()
            return Node('atom', value)
        elif self.current_token[0] == 'BOOLEAN':
            value = self.current_token[1] == 'true'
            self.next_token()
            return Node('bool', value)
        elif self.current_token[0] == 'KEY': 
            value = self.current_token[1]
            self.next_token()
            if self.current_token and self.current_token[0] == 'COLON':
               self.next_token()  
               return Node('key', value)
            return Node('string', value)
        else:
            raise ParserException(f'Unexpected token {self.current_token[1]}')

    def parse_list(self):
        self.next_token() 
        items = []
        while self.current_token and self.current_token[0] != 'RBRACKET':
            item = self.parse_data_literal()
            items.append(item)
            if self.current_token and self.current_token[0] == 'COMMA':
                self.next_token()  
        if not self.current_token or self.current_token[0] != 'RBRACKET':
            raise ParserException('List not properly terminated with ]')
        self.next_token()  
        return Node('list', items)

    def parse_tuple(self):
        items = []
        self.next_token() 
        while self.current_token is not None and self.current_token[0] != 'RBRACE':
            items.append(self.parse_data_literal())
            if self.current_token and self.current_token[0] == 'COMMA':
                self.next_token()  
        if self.current_token is None or self.current_token[0] != 'RBRACE':
            raise ParserException('Tuple not properly terminated with }')
        self.next_token() 
        return Node('tuple', [item.to_json() for item in items])

    def parse_map(self):
        self.next_token()  
        map_items = {}
        while self.current_token and self.current_token[0] != 'RBRACE':
            
            if self.current_token[0] not in {'ATOM', 'KEY'}:
                raise ParserException(f'Expected map key, found {self.current_token}')
            
            key = self.current_token[1] if self.current_token[0] == 'ATOM' else ':{}'.format(self.current_token[1])
            self.next_token()  
            
            if self.current_token and self.current_token[0] not in {'ARROW', 'COLON'}:
                raise ParserException(f'Expected “=>” or “:”, found {self.current_token}')
            self.next_token()  
            
            value = self.parse_data_literal()
            map_items[key] = value
            if self.current_token and self.current_token[0] == 'COMMA':
                self.next_token()  
        if not self.current_token or self.current_token[0] != 'RBRACE':
            raise ParserException('Map not properly terminated with }')
        self.next_token() 
        return Node('map', map_items)

def main():
    try:
        text = sys.stdin.read()
      
        tokens = list(tokenize(text))
        
        if not tokens:
            print(json.dumps([]))  
        else:
            parser = Parser(tokens)
            result = parser.parse()
            
            json_output = json.dumps([node.to_json() for node in result], separators=(',', ':'))
            print(json_output)
    except TokenizerException as e:
        sys.stderr.write(str(e) + '\n')
    except ParserException as e:
        sys.stderr.write(str(e) + '\n')


if __name__ == '__main__':
    main()
 The program above is elixir-data.py program in the prj1-sol directory. I will specifically need to submit a zip-archive which unpacks into a prj1-sol directory minimally containing a file elixir-data.ebnf plus two shell scripts make.sh and run.sh:
ndard output a s
elixir-data.ebnf must contain a grammar for the above language using the EBNF notation described in class.

Running make.sh from any directory should build any artifacts needed to run program within the prj1-sol directory.

Running run.sh from any directory should read and parse a sentence of the above language from standard input and output on staingle line containing the JSON representation of the parse.