fst.docs.d10_reconcile

Edit pure AST while preserving formatting

To be able to execute the examples, import this.

>>> from fst import *

What it does

The idea of the reconcile() functionality is not to be able to put AST nodes to an FST tree under your own control, this can easily be done. Rather it is to allow other code to modify AST trees without knowing anything about FST and then having FST reconcile these changes with what it knows about the tree to preserve formatting where it can.

>>> f = FST('''
... if i:  # 1
...   j = [f(), # 2
...        g() # 3
...       ]
... '''.strip())
>>> m = f.mark()

Notice the f.a, all changes must happen in the AST tree.

>>> f.a.body[0].value.elts[0] = Name(id='pure_ast')
>>> f.a.body.append(Assign(targets=[Name(id='k')], value=Constant(value=3)))
>>> f = f.reconcile(m)
>>> print(f.src)
if i:  # 1
  j = [pure_ast, # 2
       g() # 3
      ]
  k = 3

You can reuse and mix nodes from the original tree.

>>> m = f.mark()
>>> f.a.body.append(f.a.body[0])
>>> f.a.body[0].value.elts.append(Constant(value='pure_ast'))

We put the same AST node in two different places, this is allowed in for reconcile().

>>> f.a.body[0] is f.a.body[2]
True
>>> f = f.reconcile(m)
>>> print(f.src)
if i:  # 1
  j = [pure_ast, # 2
       g(), 'pure_ast'
      ]
  k = 3
  j = [pure_ast, # 2
       g(), 'pure_ast'
      ]

That same AST used in two places was deduplicated.

>>> f.a.body[0] is f.a.body[2]
False

You can add in AST nodes from other FST trees and they will retain their formatting, though not if you modify those ASTs since the only tree that has reconcile information to be able to preserve formatting if this is done is the original tree that was marked, for now.

>>> m = f.mark()
>>> f.a.body.append(FST('l="formatting"  # stays').a)
>>> f.a.body.append(FST('m  =  "formatting"  # disappears').a)
>>> f.a.body[-1].value = Constant(value="formatting")
>>> f = f.reconcile(m)
>>> print(f.src)
if i:  # 1
  j = [pure_ast, # 2
       g(), 'pure_ast'
      ]
  k = 3
  j = [pure_ast, # 2
       g(), 'pure_ast'
      ]
  l="formatting"  # stays
  m = 'formatting'

But if the goal is to change a small part of a larger program then this should work well enough.

>>> f = FST('''
... from .data import (
...     scalar1,  # this is 60% for now
...     scalar2,  # the rest
... )
...
... def compute(x, y):
...     # Compute the weighted sum
...     result = (
...         x * 0.6  # x gets 60%
...         + y * 0.4  # y gets 40%
...     )
...
...     # Apply thresholding
...     if (
...         result > 10
...         # cap high values
...         and result < 100  # ignore overflow
...     ):
...         return result
...     else:
...         return 0
... '''.strip())
>>> m = f.mark()
>>> f.a.body[1].body[0].value.left.right = Name(id='scalar1')
>>> f.a.body[1].body[0].value.right.right = Name(id='scalar2')
>>> f.a.body[1].body[-1].orelse[0] = (
...     If(test=Compare(left=Name(id='result'),
...                     ops=[Gt()],
...                     comparators=[Constant(value=1)]),
...        body=[f.a.body[1].body[-1].orelse[0]],
...        orelse=[Return(value=UnaryOp(op=USub(), operand=Constant(value=1)))]
...     )
... )
>>> f = f.reconcile(m)

We print like this because of doctest, just ignore.

>>> print('\n'.join(l or '.' for l in f.lines))
from .data import (
    scalar1,  # this is 60% for now
    scalar2,  # the rest
)
.
def compute(x, y):
    # Compute the weighted sum
    result = (
        x * scalar1  # x gets 60%
        + y * scalar2  # y gets 40%
    )
.
    # Apply thresholding
    if (
        result > 10
        # cap high values
        and result < 100  # ignore overflow
    ):
        return result
    elif result > 1:
        return 0
    else:
        return -1

How it works

mark() makes a copy of the tree you will reconcile later, then later reconcile() walks the modified tree from top to bottom comparing to the stored tree. Anywhere there are differences it uses the FST editing mechanisms to put the changed nodes as if a human being was making the changes.

Anytime an underlying operation fails or is unavailable (due to not being implemented yet), reconcile() simply retries the operation at a higher level node. This does lose formatting as the put() in that case winds up using the AST node, but at least it makes the operation possible.

This method is not particularly fast when there are a lot of nested changes as it is possible that large chunks of the source wind up being put multiple times with minor deviations. But it does a better job at preserving formatting than walking bottom-up.