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. let’s write the make.sh script, which in this case may be straightforward since the core of your program is a Python script (which does not require compilation). However, the make.sh script should ensure that the project is structured correctly.

make.sh:

#!/bin/bash

# Target directory for your solution
SOLUTION_DIR=“prj1-sol”

# Check if the target directory exists; create it if it doesn’t
if [ ! -d “$SOLUTION_DIR” ]; then
    mkdir complete the scripts of make.sh and run.sh