Sunday, April 26, 2009

svn-upgrade on a Bazaar Loom

I maintain modifications of Trac, a Subversion project, as a Bazaar Loom. The bottom thread mirrors the 0.11-stable branch using bzr-svn, with features built purely in Bazaar revisions on top.

When I upgraded bzr-svn once, the Subversion storage format changed incompatibly, meaning I could no longer track Subversion without rebasing my Loom. Here is how I did it:

  1. Make sure the loom is fully forward-merged so that every thread's head is a parent of the top thread. Revisions you miss won't be converted correctly.

  2. Make sure you are on the top thread. svn-upgrade only converts the HEAD and its parents, so if you aren't, the conversion will miss higher threads.

  3. bzr record the loom if there are changes.

  4. Copy or branch the loom to a backup, in case you mess up.

  5. In the Loom directory, run bzr svn-upgrade --idmap-file=../myproj.idmap http://svn.edgewall.org/repos/trac/. The idmap is for your later reference so you can fix up the loom head references, and you can choose where you want to save it. Unless you have parent_location set in .bzr/branch/branch.conf to the actual Subversion branch, you must specify the URL to the repository. This should be some parent directory of the Subversion branch URL, and is usually the one that contains the branches, tags, and trunk subdirectories.

  6. That should spend a bunch of time accessing the network.

  7. Now you need to fix up the .bzr/branch/last-loom file. This is a plain text file, and opening it you'll immediately see why we saved an idmap.

    The first line is very obviously the Loom file format, which should be "Loom current 1". The second points to the recorded loom, which you can leave alone, because we'll just be fixing where the threads point.

    Each line following is info about a thread, bottom-thread-first (completely counterintuitively, but there you are). The last revision at time of loom-record comes before " : ", the current revision after, and of course the thread name.

    Note that the top (bottom-of-file) thread is a little different. Unlike every other line, the revision ID after " : " is slightly different from the one before. That's because svn-upgrade successfully communicated to loom that the HEAD revision is now the rebased one. It should give you a hint as to how to fix the others.


  8. Looking at the idmap file, you'll quickly see a pattern. Revisions that come from Subversion have their prefixes changed, from "svn-v3-trunk0:" to "svn-v4:", and "%2F" changed to "/" in my case. Non-Subversion revisions simply have "-svn4-upgrade" appended. The whole point is for the svn-upgrade to be trivially repeatable by multiple branch owners, so either memorize the pattern, or keep this file handy.

  9. Back in last-loom, change the revids after the " : " according to the map. If your loom is like mine, with a Subversion revision as the basis and the rest in pure Bazaar, this means changing "svn-v3-trunk0" to "svn-v4" and any instances of "%2F" to "/" for the bottom thread (first in last-loom file), and adding the "-svn4-upgrade" to the rest. Do not change the revid before the " : ". Do not change line 2. Do not change the last line, because it was fixed already.

  10. Move down through the loom looking for failures of bzr log --show-ids -l1, which should print out the revids you just wrote into the loom. If there are failures, you probably messed up a replacement revision; check your idmap file again.

  11. Once you've verified everything, move back to the top thread with bzr switch and record the loom.


What about inter-Loom activity?


The whole point of repeatability in svn-upgrade is to allow independent upgrades to be shared between. So if you have people with divergent looms, simply using svn-upgrade on all of them will make the threads compatible.

This says nothing about whole-loom merging, though. Loom-recording, even between two equal looms, will create divergent loom records. I've never tried loom merging, so you may have to rough it by doing thread-only merging until everyone is synced up again.

Thursday, December 4, 2008

OLPC and the dying belief

I quoted OLPC earlier

Our commitment to software freedom gives children the opportunity to use their laptops on their own terms. While we do not expect every child to become a programmer, we do not want any ceiling imposed on those children who choose to modify their machines. We are using open-document formats for much the same reason: transparency is empowering. The children—and their teachers—will have the freedom to reshape, reinvent, and reapply their software, hardware, and content.

Short, straightforward, and powerful language.

Compare to the current page contents. Here's a sample:

Thus OLPC puts an emphasis on software tools for exploring and expressing, rather than instruction. Love is a better master than duty. Using the laptop as the agency for engaging children in constructing knowledge based upon their personal interests and providing them tools for sharing and critiquing these constructions will lead them to become learners and teachers.

As a matter of practicality and given the necessity to enhance performance and reliability while containing costs, XO is not burdened by the bloat of excess code, the “featureitis” that is responsible for much of the clumsiness, unreliability, and expense of many modern laptops.

A truly inspiring stand for constructivist teaching. Unfortunately, as far as I can tell, little else is being said.

Sunday, November 30, 2008

Understanding cl-cont semantics without thinking about CPS

Understanding the way that cl-cont transforms forms is one way to understand the sometimes counterintuitive behavior of that code. However, the difficulty of dissecting the meaning of code written in continuation-passing style is one of the major reasons that we use a CPS transformer at all instead of writing it out manually.

As a cl-cont user, you may find it easier to understand a behavior model that removes consideration of CPS entirely. After all, CPS is only the implementation method; the goal is to think of the code as "continuable".

Understanding first

To follow along with the behavior model of cl-cont, you should be familiar with dynamic context (such as that established by unwind-protect), lexical context, and the behavior of full continuations such as you would find in Scheme.

Understanding "ordinary" continuations is especially important, because cl-cont's behavior is more complex than full continuations, and you will be lost without preexisting knowledge of what they are.

Defining some terms

First, a continuation, unless otherwise qualified, is always a first-class continuation—a function that, when called, returns its arguments as values to the place where the call/cc call that created the continuation was called. This is just to be clear that I hardly ever mean something abstract when I say "continuation".

The macro with-call/cc introduces a lexical continuable context for the forms lexically contained within it. We will refer to this so often that I will call it LCC henceforth to avoid confusion. For all current purposes, it only matters whether code exists within any LCC, so you can think of "LCC-ness" as a flag on all code. We also say that any code not within an LCC is in an LNCC (lexical non-continuable context). We will define more rules for determining whether code is in an LCC later.

Entering an LCC at the beginning creates a dynamic continuable context or DCC. Understanding DCC behavior is the key to understanding cl-cont. Unlike LCC, you can have multiple distinct DCCs active at any time, possibly even sharing code, just as one function calling mapcar doesn't preclude you from doing so. All code not executing within a DCC is executing within a DNCC (dynamic non-continuable context).

Lexical continuable contexts

As I have said, with-call/cc introduces an LCC. This is an implicit progn. A few convenient macros, such as lambda/cc and defun/cc, wrap their non-cc counterparts with with-call/cc. Within this form, an LNCC can be inserted with the macro without-call/cc. The pseudo-function call/cc also implies a without-call/cc around its sole argument. Within any LNCC, including the implicit one around every program, further LCCs may be introduced with with-call/cc.

There are some cases where you might think that an LCC is there or doesn't matter, where it actually does. In this code:

(defun x ()
(with-call/cc
1 2))

The LCC only contains the 1 and 2 forms, not the function entry and function exit. Therefore, calling X from any code will result in first executing code in an LNCC, then some LCC code, then some LNCC code. The importance of this distinction will become clear once we start discussing DCCs. To solve this, move the with-call/cc to outside the defun, or use the equivalent defun/cc convenience macro.

A similar situation holds for this code:

(lambda () (with-call/cc 3 4))

Calling this function is the same as calling X above, and the same solutions apply.

One final area of confusion may be:

(with-call/cc
(without-call/cc
(with-call/cc 5 6)))

This is not a smooth contour of an LCC; the without-call/cc always creates an LNCC, even if a new one is created immediately therein.

Basic DCC execution

Entering any LCC from a DNCC, by either calling a function created or defined in an LCC or simply evaluating a with-call/cc form, creates a DCC. Upon creation, beyond the usual frame info, one property is captured and saved permanently for that DCC: the exit point. Consider this code:

(defun/cc bob ()
(cons 4 2)
:bob)
(defun/cc slack ()
(bob)
:slack)
(defun moo ()
(bob)
(slack))

Calling bob in moo creates a DCC whose exit point is the code that returns the keyword :bob. Calling slack in moo creates another one whose exit point is returning :slack. The call to bob within the call to slack does not create a new DCC, or even alter the existing one! Whether calling bob creates a new DCC depends on the nature of the calling code.

While in a DCC, calling a function defined in an LNCC suspends that DCC. That happens in bob above when calling cons, which, like all CL standard functions, is defined in some LNCC. Whether the compiler optimizes away the cons is irrelevant to our semantic model. When the function returns, the same DCC is resumed; as such, the cons above destroys neither DCC's exit point information.

The function rule sounds like a special case, but it is really just a subcase of the continuation case. Invoking a continuation enters the DCC in which it was created. In cl-cont, calling LNCC functions is handled by grabbing the continuationand invoking it with the function's result after it finishes.

After exiting a DCC, the only way to reenter it is to invoke one of its continuations. Calling a function defined therein only creates a new DCC delimited by that function's body.

Finally, a point about call/cc that will surprise you if you are used to Scheme: Invoking call/cc exits the DCC, returning the values returned by the function you gave it. You are, of course, free to simulate Scheme behavior by invoking the continuation from right within that function.

Strange exit point behavior

This may seem a little surprising:

(defvar *cc*)
(defun/cc foo ()
(let/cc cc (setf *cc* cc) 'saved)
'foo)

(defun/cc bar ()
(foo)
'bar)

(progn
(foo) ; ⇒SAVED
(funcall *cc*) ; ⇒FOO
(bar) ; ⇒SAVED
(funcall *cc*) ; ⇒BAR
)

The important thing to remember is that it doesn't matter at all what context you invoke a continuation in from a DNCC; you always get the exit point of the continuation's DCC.

Nested DCCs

Certain code looks like it should really be resuming something that it actually can't, and it has to do with nesting DCCs. Here's what I mean:

(let (keep-going list)
(progn (with-call/cc
(setf list (mapcar (lambda (n)
(let/cc k (setf keep-going k) 42))
(list 1 2 3 4 5))))
(dotimes (n 5)
(funcall keep-going (+ n 6)))
list))

If mapcar was defined in an LCC, you would get the result you expect, (6 7 8 9 10). What you expect is that for each element in (1 2 3 4 5), let/cc saves and suspends the continuation.. The first time, the dotimes would be entered, and the next four times it would do another iteration, each time invoking the continuation saved above. In the first case, the 42 would be delivered to the first progn location (and discarded), and the further four times to the implicit progn in dotimes (and discarded).

Instead, you get the result (42 42 42 42 42). Why?

The mapcar throws a kink in. Consider that when mapcar is running, the enclosing DCC is suspended, because mapcar is defined in an LNCC and therefore creates a DNCC by being called. Now, when it calls the LCC-defined function passed to it, a new DCC is created each time. In each of these a continuation is saved and 42 is returned, creating the contents of the list, but only the last saved continuation is ever seen by the dotimes. When mapcar finishes, it resumes the enclosing continuation and it finishes execution before the dotimes can even start.

Now that dotimes has started, it is resuming the DCC solely delimited by the function passed to mapcar. As you can see, the exit point of that function is to return whatever was passed to the continuation. Accordingly, if you wrap the funcall above with a print, you'll see it print 6, 7, 8, 9, and 10, in order.

Saturday, October 11, 2008

A practical use for change-class!

Previously only thought useful for metaprogramming, the amazing change-class finds a use in the hallowed realm of application programming.

(defwidget maybe-pagination (weblocks:pagination)
((show-pred :initarg :show-predicate :accessor show-paginator-predicate
:initform (constantly t)
:documentation "One-arg proc accepting self answering
whether to show the widget contents."))
(:documentation "Hide the paginator sometimes."))

(defmethod weblocks:render-widget-body
((self maybe-pagination) &key &allow-other-keys)
"Iff `show-paginator-predicate''s value answers NIL on myself, don't
send to super."
(when (funcall (show-paginator-predicate self) self)
(call-next-method)))

(defmethod initialize-instance :after
((self my-listedit-subclass) &key &allow-other-keys)
(change-class (weblocks:dataseq-pagination-widget self) 'maybe-pagination
:show-predicate (f_% (typep (weblocks:authenticatedp) 'admin-account))))

Wow, huh?

(The above makes it so that only admins can see the subwidget that
shows the current page with links for paging through the results.)

Tuesday, September 16, 2008

What is cascading rebase?

The problem of cascading rebase is a serious one, and should not be taken lightly. Though the idea of cleaning history may be appealing, it is almost always inappropriate.

This problem is what makes all documenters of rebase functionality implore you to avoid rebasing public repositories. However, the nature of the problem is difficult to explain to those without a good understanding of the DAG-based branching and merging employed by most modern distributed VCSes, including Bazaar, Mercurial, and Git.

What starts simple…



Imagine a larger version of this very abbreviated history. Bob has started a project and commits new revisions to the mainline, and collects contributions by merging from others' branches.

Here we see Bob's mainline in red.



Topic branching



Alice has branched mainline-3 to create a feature, and made a few commits. The idea is that Bob will merge the feature branch, in green, back into mainline. Work on mainline can continue, as seen by additional mainline revisions.



Uh oh…



Bob realizes that mainline-2 was done wrong. He decides the problem is serious enough that it has to be redone.

This is not the only kind of rebasing; rebasing is a subsequence replacement operation, so it can be used to insert revs in history, delete revs from history, or insert and replace any number of revs at any point. In this case, we're replacing mainline-2 with mainline-2'.

Here is the completed rebase operation. The dead revisions are shown in light red.



Rebasing is really branching



Every revision in a proper DAG history contains an immutable reference to its parents. So mainline-3 has a reference to mainline-2. But we rewrote mainline-2, so we must rebuild mainline-3 so it refers to the new mainline-2 instead, 4 to 3, 5 to 4, and so on.

As such, rebasing doesn't really "rewrite history"; it finds the latest revision that doesn't have to be rewritten, branches off it, builds the new history onto that branch, and finally replaces the current branch with the new branch. You can see this clearly in an alternate, but equivalent visualization of the previous graph.



Why is the feature branch still connected to the dead revisions?



The rules don't just apply to mainline; feature-4 also has a reference to mainline-3. This is an important integrity feature—as a brancher working on a private feature, Alice wouldn't want Bob to be able to spontaneously rewrite her branch's source by altering revisions made before she branched. Therefore, those "dead" revisions still live as part of the feature branch's history.

I show them above the feature branch here to show how they have become part of Alice's history. They will also be shared with any other branches that were made off mainline after mainline-2 and before the rebase. Please note that the history of Alice's branch remains exactly the same as it was before the rebase.



What happens with a merge?



Feature branches live to die; they ought to be merged back into the mainline eventually, when the feature is ready to be part of it. So Alice would like Bob to merge her feature branch into mainline.

Except the idea of mainline has changed due to the rebase. Here is a symmetric merge diagram to illustrate.



One step in the cascade occurs



Alice by necessity includes the broken changes from the old mainline-2 and the mostly duplicate mainline-3. The DAG sees these as separate from mainline-2' and mainline-3', as they are. So the merge is wrong.

To fix this, Alice must produce a new branch and rewrite her changes onto it. We can do this with rebase, but it requires Alice to know which revisions are duplicates. Here, Alice must know that mainline-3' is the new basis. This seems simple, but imagine if mainline-2 had been simply deleted, or some new revisions had been inserted. Then the revisions would be numbered differently; in that case she would have to rebase from mainline-3 to mainline-2'.

Here is the result of her rebase, and the correct merge.



Now the cascade happens: if any topic branches were made from the "dead" feature-4, feature-5, and feature-6, they must also be rebased onto the new feature-4', 5', or 6', as appropriate. And so on. And so on. Hence my name for this issue, which I don't believe has been adopted by anyone but appropriately illustrates its seriousness, cascading rebase.

Cascading rebase cannot be solved automatically; all rebase tools recognize this and have some interactive conflict resolution features. Furthermore, there is no guarantee that when a rebase goes smoothly on the original branch, it will also go so smoothly on the cascade. It also gets wildly complicated to calculate the cascade when you include behavior like synchronization by merging and especially cross-merging.

Before you consider rebasing, consider other tools for fixing problems. If you made a mistake in an old revision, and it is serious enough that all branches since should receive the fix as well, a good alternative is to branch off the broken revision, write the fix, commit to the new branch, merge it into mainline, publish the revision, and ask others to merge it in. Even if they don't, the common history will mean that merging won't see a parallel "fixed" revision, making the merge cleaner and less likely to conflict.

You don't even have to create a new URL in many cases. In both Bazaar and Mercurial, the commit to the side branch will receive a revision number in your mainline. By merging the mainline at this revision number, branches will receive only that revision, without being forced to merge the head of mainline.