First cut at automatic AST generation with piyacc.py --element switch
authorNick Downing <downing.nick@gmail.com>
Wed, 12 Sep 2018 13:02:02 +0000 (23:02 +1000)
committerNick Downing <downing.nick@gmail.com>
Wed, 12 Sep 2018 13:04:28 +0000 (23:04 +1000)
.gitignore
generate_bison.py
generate_py.py
piyacc.py
skel/skel_py.py
skel/skel_py_element.py [new file with mode: 0644]
tests_ast/Makefile [new file with mode: 0644]
tests_ast/cal.py [new file with mode: 0755]
tests_ast/cal_py.l [new file with mode: 0644]
tests_ast/cal_py.y [new file with mode: 0644]
tests_ast/element.py [new file with mode: 0644]

index 07c2cb7..92f8b71 100644 (file)
@@ -9,7 +9,11 @@ skel/skel_bison.c.orig
 skel/skel_bison.h.orig
 tests/*.c
 tests/*.o
-tests/*.py
 tests/*.xml
+tests/lex_yy.py
+tests/y_tab.py
 tests/cal
 tests/cal2
+tests_ast/*.xml
+tests_ast/lex_yy.py
+tests_ast/y_tab.py
index 7929cce..ac96e90 100644 (file)
@@ -14,7 +14,14 @@ escapes = {
   0x5c: '\\\\'
 }
 
-def generate_bison(_ast, home_dir, skel_file, out_file, defines_file = None):
+def generate_bison(
+  _ast,
+  _element,
+  home_dir,
+  skel_file,
+  out_file,
+  defines_file = None
+):
   _lr1dfa = _ast.to_lr1().to_lalr1()
 
   # generate translate table for terminal symbols
index e3f1ccd..577b0a0 100644 (file)
@@ -31,7 +31,14 @@ def ast_text_to_python(ast_text, indent):
       lines[j] = '{0:s}{1:s}\n'.format(indent, lines[j][len(prefix):])
   return ''.join(lines)
 
-def generate_py(_ast, home_dir, skel_file, out_file, defines_file = None):
+def generate_py(
+  _ast,
+  _element,
+  home_dir,
+  skel_file,
+  out_file,
+  defines_file = None
+):
   _lr1dfa = _ast.to_lr1().to_lalr1()
   assert _lr1dfa.eof_terminal == 0
   actions = [i for _, i in _lr1dfa.productions]
@@ -41,7 +48,10 @@ def generate_py(_ast, home_dir, skel_file, out_file, defines_file = None):
   ]
 
   if skel_file is None:
-    skel_file = os.path.join(home_dir, 'skel/skel_py.py')
+    skel_file = os.path.join(
+      home_dir,
+      'skel/skel_py_element.py' if _element else 'skel/skel_py.py'
+    )
   if out_file is None:
     out_file = 'y_tab.py'
   with open(skel_file, 'r') as fin:
index b651c2f..cdb66f2 100755 (executable)
--- a/piyacc.py
+++ b/piyacc.py
@@ -12,14 +12,15 @@ home_dir = os.path.dirname(sys.argv[0])
 try:
   opts, args = getopt.getopt(
     sys.argv[1:],
-    'do:pS:',
-    ['defines=', 'outfile=', 'python', 'skel=']
+    'edo:pS:',
+    ['element', 'defines=', 'outfile=', 'python', 'skel=']
   )
 except getopt.GetoptError as err:
   sys.stderr.write('{0:s}\n'.format(str(err)))
   sys.exit(1)
 
 defines_file = None
+_element = False
 out_file = None
 python = False
 skel_file = None
@@ -28,6 +29,8 @@ for opt, arg in opts:
     defines_file = 'y.tab.h'
   elif opt == '--defines':
     defines_file = arg
+  elif opt == '-e' or opt == '--element':
+    _element = True
   elif opt == '-o' or opt == '--outfile':
     out_file = arg
   elif opt == '-p' or opt == '--python':
@@ -54,6 +57,7 @@ _ast.post_process()
 #_ast = element.deserialize('b.xml', ast.factory, 'utf-8')
 (generate_py.generate_py if python else generate_bison.generate_bison)(
   _ast,
+  _element,
   home_dir,
   skel_file,
   out_file,
index c80d2e3..db0b12b 100644 (file)
@@ -4,7 +4,7 @@ import bisect
 
 # GENERATE TOKENS
 
-yystack = []
+yystack = None
 yytoken = None
 
 yyval = None
diff --git a/skel/skel_py_element.py b/skel/skel_py_element.py
new file mode 100644 (file)
index 0000000..e9da944
--- /dev/null
@@ -0,0 +1,91 @@
+import bisect
+import element
+
+# GENERATE SECTION1
+
+# GENERATE TOKENS
+
+yystack = None
+yytoken = None
+
+yyval = None
+yyloc = None
+
+yylval = None
+yylloc = None
+
+yy_element_stack = None
+yy_element_space = None
+yy_element_token = None
+
+# GENERATE SECTION2
+
+def yyparse():
+  global yystack, yytoken, yyval, yyloc, yylval, yylloc
+  global yy_element_stack, yy_element_space, yy_element_token
+
+  # GENERATE INITIALACTION
+
+  state = 0
+  yystack = []
+  yylval = None
+  yytoken = -1
+  yy_element_stack = []
+  while True:
+    #print('state', state, 'yystack', yystack)
+    reduce = yy_lr1dfa_states[state][4]
+    if reduce == -1:
+      if yytoken == -1:
+        yylval = None
+        yylloc = None
+        yytoken = yylex()
+        #print('yytoken', yytoken, 'yylval', yylval, 'yylloc', yylloc)
+      action = yy_lr1dfa_states[state][1][
+        bisect.bisect_right(yy_lr1dfa_states[state][0], yytoken)
+      ]
+      if action == -1:
+        raise Exception('syntax error')
+      if (action & 1) == 0:
+        yystack.append((state, yylval, yylloc))
+
+        # push space then AST element contiguously onto yy_element_stack
+        # even numbered elements are spaces, odd numbered elements are AST
+        yy_element_stack.append(yy_element_space)
+        yy_element_stack.append(yy_element_token)
+
+        state = action >> 1
+        #print('shift', state)
+        yytoken = -1
+        continue
+      reduce = action >> 1
+    #print('reduce', reduce)
+    len_symbols, ref_data = yy_lr1dfa_productions[reduce]
+    base = len(yystack) - len_symbols
+    yystack.append((state, None, None))
+    state, yyval, yyloc = yystack[base]
+    ref_data()
+    del yystack[base:]
+    if reduce == 0:
+      assert base == 0
+      break
+    yystack.append((state, yyval, yyloc))
+
+    # concatenate yy_element_stack[base * 2:] to space then AST element
+    j = base * 2 + 1
+    for i in range(j + 1, len(yy_element_stack)):
+      k = len(yy_element_stack[j])
+      element.set_text(
+        yy_element_stack[j],
+        k,
+        element.get_text(yy_element_stack[j], k) +
+        element.get_text(yy_element_stack[i], 0)
+      )
+      yy_element_stack[j][k:] = yy_element_stack[i][:]
+    del yy_element_stack[j + 1:]
+
+    state = yy_lr1dfa_states[state][3][
+      bisect.bisect_right(yy_lr1dfa_states[state][2], reduce)
+    ]
+    assert state != -1
+
+# GENERATE SECTION3
diff --git a/tests_ast/Makefile b/tests_ast/Makefile
new file mode 100644 (file)
index 0000000..f245b74
--- /dev/null
@@ -0,0 +1,12 @@
+all: lex_yy.py y_tab.py
+
+lex_yy.py: cal_py.l
+       ../../bootstrap_flex.git/src/flex -o /dev/null $< 2>$<.xml
+       ../../pilex.git/pilex.py --element --python $<.xml
+
+y_tab.py: cal_py.y
+       ../../bootstrap_bison.git/src/bison -o /dev/null $< 2>$<.xml
+       ../piyacc.py --element --python $<.xml
+
+clean:
+       rm -f lex_yy.py y_tab.py *.xml
diff --git a/tests_ast/cal.py b/tests_ast/cal.py
new file mode 100755 (executable)
index 0000000..fd1a7b9
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+
+import element
+import sys
+import y_tab
+
+sys.stdout.write('Enter the expression: ')
+sys.stdout.flush()
+y_tab.yyparse()
+sys.stdout.write(
+  'space \'{0:s}\'\nAST \'{1:s}\'\n'.format(
+    element.get_text(y_tab.yy_element_stack[0], 0),
+    element.get_text(y_tab.yy_element_stack[1], 0)
+  )
+)
diff --git a/tests_ast/cal_py.l b/tests_ast/cal_py.l
new file mode 100644 (file)
index 0000000..272fba6
--- /dev/null
@@ -0,0 +1,16 @@
+%{
+import y_tab
+%}
+
+DIGIT [0-9]+\.?|[0-9]*\.[0-9]+
+
+%%
+
+[ ]
+{DIGIT}        {
+  y_tab.yylval = float(yytext)
+  return y_tab.NUM
+}
+\n|.   {
+  return ord(yytext[0])
+}
diff --git a/tests_ast/cal_py.y b/tests_ast/cal_py.y
new file mode 100644 (file)
index 0000000..4fea55a
--- /dev/null
@@ -0,0 +1,36 @@
+%{
+from lex_yy import yylex
+import sys
+%}
+%token NUM
+
+%left '+' '-'
+%left '*' '/'
+%right UMINUS
+
+%%
+
+S : S E '\n' {
+  sys.stdout.write('Answer: {0:g}\nEnter:\n'.format($2))
+}
+  | S '\n'
+  |
+  | error '\n' {
+  yyerror('Error: Enter once more...\n')
+  yyerrok()
+}
+  ;
+E : E '+' E { $$ = $1 + $3 }
+  | E '-' E { $$ = $1 - $3 }
+  | E '*' E { $$ = $1 * $3 }
+  | E '/' E { $$ = $1 / $3 }
+  | '(' E ')' { $$ = $2 }
+  /*| '-' E %prec UMINUS { $$ = -$2 }*/
+  | '-' { print('unary minus', $1) } E %prec UMINUS { $$ = -$3 }
+  | NUM
+  ;
+%%
+
+def yyerror(s):
+  sys.stdout.write('{0:s}\n'.format(s))
+  sys.exit(1)
diff --git a/tests_ast/element.py b/tests_ast/element.py
new file mode 100644 (file)
index 0000000..1f8e84f
--- /dev/null
@@ -0,0 +1,157 @@
+import xml.etree.ElementTree
+
+class Element(xml.etree.ElementTree._Element_Py):
+  def __init__(self, tag = 'Element', attrib = {}, text = '', children = []):
+    xml.etree.ElementTree._Element_Py.__init__(self, tag, attrib)
+    self.ref = -1
+    self.seen = False
+    set_text(self, 0, text)
+    self[:] = children
+  def serialize(self, ref_list):
+    for i in self:
+      # parented, enforce that child can only be parented at most once
+      # (although there can be unlimited numbers of numeric refs to it)
+      assert not i.seen
+      i.seen = True
+      if i.ref == -1:
+        i.serialize(ref_list)
+  def deserialize(self, ref_list):
+    for i in self:
+      i.deserialize(ref_list)
+  def copy(self, factory = None):
+    result = (Element if factory is None else factory)(self.tag, self.attrib)
+    result.text = self.text
+    result.tail = self.tail
+    result[:] = [i.copy() for i in self]
+    return result
+  def repr_serialize(self, params):
+    if len(self):
+      params.append(
+        'children = [{0:s}]'.format(
+          ', '.join([repr(i) for i in self])
+        )
+      )
+  def __repr__(self):
+    params = []
+    self.repr_serialize(params)
+    return 'element.Element({0:s})'.format(', '.join(params))
+
+bool_to_str = ['false', 'true']
+def serialize_bool(value):
+  return bool_to_str[int(value)]
+
+str_to_bool = {'false': False, 'true': True}
+def deserialize_bool(text):
+  return str_to_bool[text]
+
+def serialize_int(value):
+  return str(value)
+
+def deserialize_int(text):
+  return int(text)
+
+def serialize_ref(value, ref_list):
+  if value is None:
+    ref = -1
+  else:
+    ref = value.ref
+    if ref == -1:
+      ref = len(ref_list)
+      ref_list.append(value)
+      value.ref = ref
+      value.set('ref', str(ref))
+      # this doesn't set the seen flag, so it will be parented by the
+      # root, unless it is already parented or gets parented later on
+      if not value.seen:
+        value.serialize(ref_list)
+  return str(ref)
+
+def deserialize_ref(text, ref_list):
+  ref = int(text)
+  return None if ref < 0 else ref_list[ref]
+
+def serialize_str(value):
+  return value
+
+def deserialize_str(text):
+  return text
+
+def serialize(value, fout, encoding = 'unicode'):
+  ref_list = []
+  serialize_ref(value, ref_list)
+  parents = [i for i in ref_list if not i.seen]
+  root = Element('root', children = parents)
+  for i in range(len(root)):
+    set_text(root, i, '\n  ')
+  set_text(root, len(root), '\n')
+  root.tail = '\n'
+  xml.etree.ElementTree.ElementTree(root).write(fout, encoding)
+  for i in root:
+    i.tail = None
+  for i in ref_list:
+    i.ref = -1
+    del i.attrib['ref']
+  i = 0
+  while i < len(parents):
+    for j in parents[i]:
+      j.seen = False
+      parents.append(j)
+    i += 1
+
+def deserialize(fin, factory = Element, encoding = 'unicode'):
+  root = xml.etree.ElementTree.parse(
+    fin,
+    xml.etree.ElementTree.XMLParser(
+      target = xml.etree.ElementTree.TreeBuilder(factory),
+      encoding = encoding
+    )
+  ).getroot()
+  assert root.tag == 'root'
+  for i in root:
+    i.tail = None
+  i = 0
+  parents = root[:]
+  ref_list = []
+  while i < len(parents):
+    j = parents[i]
+    if 'ref' in j.attrib:
+      ref = int(j.attrib['ref'])
+      del j.attrib['ref']
+      if len(ref_list) < ref + 1:
+        ref_list.extend([None] * (ref + 1 - len(ref_list)))
+      ref_list[ref] = j
+    parents.extend(j[:])
+    i += 1
+  for i in root:
+    i.deserialize(ref_list)
+  return ref_list[0]
+
+# compatibility scheme to access arbitrary xml.etree.ElementTree.Element-like
+# objects (not just Element defined above) using a more consistent interface:
+def get_text(root, i):
+  if i < 0:
+    i += len(root) + 1
+  text = root.text if i == 0 else root[i - 1].tail
+  return '' if text is None else text
+
+def set_text(root, i, text):
+  if i < 0:
+    i += len(root) + 1
+  if len(text) == 0:
+    text = None
+  if i == 0:
+    root.text = text
+  else:
+    root[i - 1].tail = text
+
+def to_end_relative(root, pos, off):
+  assert pos >= 0 and off >= 0
+  off -= len(get_text(root, pos))
+  pos -= len(root) + 1
+  return pos, off
+
+def to_start_relative(root, pos, off):
+  assert pos < 0 and off <= 0
+  pos += len(root) + 1
+  off += len(get_text(root, pos))
+  return pos, off