ENB: Momentous Ahas re vnodes

85 views
Skip to first unread message

Edward K. Ream

unread,
Jul 28, 2023, 8:18:01 AM7/28/23
to leo-editor

This Engineering Notebook post announces several momentous Ahas, all directly related to VNodes and our attitude towards them. These Ahas resolve long-standing questions and suggest new ways of simplifying Leo's most fundamental code.


Acknowledgements: These Ahas would not have happened with recent discussions with Félix and Vitalije.


Aha! The best way to check outline structure is to test the contents of the v.parents and v.children arrays.


The new c.checkVnodeLinks method is 100 times faster than previous checks and finds problems that have lain hidden until now. Leo performs these checks when reading, pasting or saving outlines.


The read check has already failed when loading an outline after switching branches. The paste checks cause two paste-related unit tests to fail.


Aha! c.checkVnodeLinks can repair broken or missing links.


The unified PR contains prototype code. I'll enable this code only after it passes a strict new unit test.


Aha! VNodes are the proper context for most low-level operations.


I've never realized how true this is until now.


Aha! The c.all_unique_nodes generator (or something similar, see below) should be the basis for many VNode operations.


Relying on c.all_unique_nodes is a new pattern for me! For example, c.checkVnodeLinks now uses c.all_unique_nodes instead of a bespoke recursion on v.children.


Aha! I now know how to backup/restore a VNode and its descendants!


Neither recursion nor pickling will work because VNodes contain backlinks in their v.parents arrays. Instead, Leo's new archive code will create a json-like dict in which almost all keys and values will be gnxs!


Define p.archive (yes, p) something like this (untested, but the code passes mypy):


def archive(self) -> dict[str, Any]:

    """Return a json-like archival dictionary for p/v.unarchive."""

    p = self


    # Create a list of all vnodes in p.self_and_subtree.

    all_unique_vnodes: list[VNode] = []

    for p in p.self_and_subtree():

        if p.v not in all_unique_vnodes:

            all_unique_vnodes.append(p.b)


    # Create an archive of all_vnodes.

    parents_dict: dict[str, list[str]] = {}

    for v in all_unique_vnodes:

        parents_dict [v.gnx] = [z.gnx for z in v.parents]


    children_dict: dict[str, list[str]] = {}

    for v in all_unique_vnodes:

        children_dict [v.gnx] = [z.gnx for z in v.children]


    marks_dict: dict[str, str] = {}

    for v in all_unique_vnodes:

        marks_dict [v.gnx] = str(int(v.isMarked()))


    uas_dict: dict[str, dict] = {}

    for v in all_unique_vnodes:

        uas_dict [v.gnx] = v.archive_ua() # To do.


    return {

        'vnodes': all_unique_vnodes,

        'parents': parents_dict,

        'children': children_dict,

        'marks': marks_dict,

        'uAs': uas_dict,

    }


Summary


c.checkVnodeLinks reveals previous hidden errors in Leo's code. Leo's codebase is about to become significantly more solid!


c.checkVnodeLinks can correct any broken or missing links in a straightforward way. c.checkVnodeLinks will raise ValueError during unit tests :-)


A generator on unique vnodes is the natural way to accumulate archival data. These data should be a json-like dict in which all keys will be gnxs and many values will be lists of gnxs.


p.archive and v.archive_uas will likely replace ad-hoc code scattered throughout Leo's codebase.


I shall merge the unified PR into devel later today. There is no reason to delay. I'll then create new issues and PRs:


- Fix the failing unit tests related to pasting nodes.

- Create a stringent new unit test for repairing links.

- Enable automatic link repair.

- Experiment with archives.


Edward

Edward K. Ream

unread,
Jul 28, 2023, 8:19:48 AM7/28/23
to leo-editor
On Friday, July 28, 2023 at 7:18:01 AM UTC-5 Edward K. Ream wrote:

> Acknowledgements: These Ahas would not have happened without the recent discussions with Félix and Vitalije.

Edward

Edward K. Ream

unread,
Jul 28, 2023, 10:27:28 AM7/28/23
to leo-e...@googlegroups.com
On Fri, Jul 28, 2023 at 7:18 AM Edward K. Ream <edre...@gmail.com> wrote:

> This Engineering Notebook post announces several momentous Ahas, all directly related to VNodes and our attitude towards them.

Several notes:

1. I have just merged the unified PR into devel. This merge will require some cleanups. I'll tend to these tasks immediately.

2. Hurray! Leo has just reported link errors after I switched branches.

I created a copy of my local leoPy.leo for study. Leo wouldn't let me save a "corrupt" outline(!), so I used copy/paste. Even better, Leo continues to report the link errors, so I should be able to nail down what happened.

3. I forgot to mention why p.archive can represent VNodes with gnxs. The reason is that c.fileCommands.gnxDict contains all vnodes, including deleted VNodes! But I've got to check this statement :-)

4. The basis for the VNode checks is the parent/child constraint:

for child in parent.children:
    parent.children.count(child) == child.parents.count(parent)

In other words, Leo should update v.parents and v.children arrays "symmetrically." Like this:

parent.children.append(child)
child.parents.append(parent)

Alas, not all of Leo's code updates these arrays this way. Leo's FastRead code is a prime offender.

Summary

At last we have a way to fix #1183!! But the fix will not be trivial.

Edward

Edward K. Ream

unread,
Jul 28, 2023, 10:46:07 AM7/28/23
to leo-editor
On Friday, July 28, 2023 at 9:27:28 AM UTC-5 Edward K. Ream wrote:

> Hurray! Leo has just reported link errors after I switched branches.

For the record, here is the report, slightly cleaned up:

Mismatch between parent.children and child.parents
parent: u.Externally visible entries   count(parent.children) = 1
 child: u.restoreFromCopiedTree        count(child.parents) = 0

Mismatch between parent.children and child.parents
parent: u.afterX...                    count(parent.children) = 1
 child: u.afterChangeTree              count(child.parents) = 0

Mismatch between parent.children and child.parents
parent: u.beforeX...                   count(parent.children) = 1
 child: u.beforeChangeTree             count(child.parents) = 0

Mismatch between parent.children and child.parents
parent: u.redo helpers                 count(parent.children) = 1
 child: u.redoChangeTree (to do)       count(child.parents) = 0

checkOutline 0.06 sec. leoPy.leo getLeoFile

It's easy to correct such defects. Just add parent.v to child.parents!

Other cases are a bit trickier,  but Leo can correct all link problems.

Edward

Edward K. Ream

unread,
Jul 28, 2023, 4:36:51 PM7/28/23
to leo-editor
On Friday, July 28, 2023 at 9:27:28 AM UTC-5 Edward K. Ream wrote:
 
> I have just merged the unified PR into devel. This merge will require some cleanups.

Done at rev d5d3a2 in devel. This rev suppresses the new vnode checks and skips several new unit tests that would otherwise fail.

Please keep testing devel :-)

Edward

HaveF HaveF

unread,
Jul 28, 2023, 9:30:32 PM7/28/23
to leo-editor
Maybe this file should not be in repo?

leo/core/fail-leoPy.leo

Edward K. Ream

unread,
Jul 29, 2023, 3:52:18 AM7/29/23
to leo-e...@googlegroups.com
On Fri, Jul 28, 2023 at 8:30 PM HaveF HaveF <iamap...@gmail.com> wrote:
Maybe this file should not be in repo?

 leo/core/fail-leoPy.leo is a temporary measure.  For now, it is essential for testing.

Edward

HaveF HaveF

unread,
Jul 29, 2023, 7:22:52 AM7/29/23
to leo-e...@googlegroups.com
 leo/core/fail-leoPy.leo is a temporary measure.  For now, it is essential for testing.
I see. 

Edward K. Ream

unread,
Jul 30, 2023, 7:37:16 AM7/30/23
to leo-e...@googlegroups.com
On Fri, Jul 28, 2023 at 7:18 AM Edward K. Ream <edre...@gmail.com> wrote:

> This Engineering Notebook post announces several momentous Ahas all directly related to VNodes.
...

> Define p.archive (yes, p) something like this (untested, but the code passes mypy):

 
def archive(self) -> dict[str, Any]:
    """Return a json-like archival dictionary for p/v.unarchive."""
    p = self

    # Create a list of all vnodes in p.self_and_subtree.
    all_unique_vnodes: list[VNode] = []
    for p in p.self_and_subtree():
        if p.v not in all_unique_vnodes:
            all_unique_vnodes.append(p.b)  <--- Oops, I meant p.v

The code above contains a subtle bug. The all_unique_vnode list is incomplete. It should also contain (at minimum) all vnodes in all v.parents and v.children arrays.

Instead, p.archive should be something like this (untested, but passes mypy):

def archive(self) -> dict[str, Any]:
    """Return a json-like archival dictionary for p/v.unarchive."""
    p = self
    c = p.v.context

    # Create an *initial* list of all vnodes in p.self_and_subtree.

    all_unique_vnodes: list[VNode] = []
    for p in p.self_and_subtree():
        if p.v not in all_unique_vnodes:
            all_unique_vnodes.append(p.v)
           
    def ref(v: VNode) -> Optional[str]:
        if v == c.hiddenRootNode:
            return None
        if v.gnx not in all_unique_vnodes:
            all_unique_vnodes.append(v.gnx)
        return v.gnx


    parents_dict: dict[str, list[str]] = {}
    for p2 in p.self_and_subtree():
        v = p2.v
        parents_list = [ref(z) for z in v.parents]
        parents_dict[v.gnx] = [z for z in parents_list if z]


    children_dict: dict[str, list[str]] = {}
    for p2 in p.self_and_subtree():
        v = p2.v
        childrens_list = [ref(z.gnx) for z in v.children]
        children_dict[v.gnx] = [z for z in childrens_list if z]


    marks_dict: dict[str, str] = {}
    for v in all_unique_vnodes:
        marks_dict[v.gnx] = str(int(v.isMarked()))

    uas_dict: dict[str, dict] = {}
    for v in all_unique_vnodes:
        uas_dict[v.gnx] = v.archive_ua()  # To do.

    return {
        'vnodes': all_unique_vnodes,
        'parents': parents_dict,
        'children': children_dict,
        'marks': marks_dict,
        'uAs': uas_dict,
    }


I think this strategy is enough.  However, this code does not necessarily archive all the vnodes contained in the parents and child arrays! To do that we would need to compute the transitive closure of all_unique_vnodes. In general, that would be all the vnodes in the outline!

Notice that the new code computes the parents and children dicts by iterating on:

    for p2 in p.self_and_subtree():

rather than:

    for v in all_unique_vnodes:

The latter would start to compute a transitive closure!

Summary

We probably don't need the transitive closure of all vnodes when doing a paste-retaining-clones when the source and target outlines are the same. I'm not sure about cross-outline paste-retaining-clones.

In short, all aspects of archiving nodes are tricky :-)

Edward

Edward K. Ream

unread,
Aug 1, 2023, 7:18:29 AM8/1/23
to leo-editor
On Sunday, July 30, 2023 at 6:37:16 AM UTC-5 Edward K. Ream wrote:


> Define p.archive (yes, p) something like this.


PR #3473 contains the latest code. I'll say no more here.


Edward

Reply all
Reply to author
Forward
0 new messages