So you launch the full example (without extracting the number parser) and you
get this:

> ruby simple.rb
Duplicate subtrees while merging result of
IFTHENELSE
only the values of the latter will be kept. (keys: [:number])
Duplicate subtrees while merging result of
IFTHENELSE
only the values of the latter will be kept. (keys: [:number])
{:number=>"3"@17}

The warnings that parslet outputs can be shut up with -W0 on the
command line, but the result is hardly what we expect. (what about 1 and 2?)
So the warnings probably mean something. To explain this, I’d like to dive
into what parslet does with the output from its parsing stages (also called
atoms).

Output Merging Algorithm (Handwaving Version)

Let’s consider a parser that has no tags (.as(...)) in it. The
two basic atoms of such a parser will have strings as output:

str('a').parse('a') # => "a"@0
match['a'].parse('a') # => "a"@0

This is always the case. As shown above, the entire parser will have the
input string as output. Here are the stages such an input string goes through
before being returned to you:

From this, we’ll deduce merge rule #1: Elements of a sequence or a
repetition match are concatenated if they’re strings. This rule is enough to
explain the first result we had above where our :ifthenelse rule
would return the string we fed into it.

Let’s inspect what happens when we start tagging. The most interesting case is
partial tagging, since that is what occurs most often in real life. The things
we parse contain delimiters that are not signal, but syntactic noise:

And here’s rule #3: In sequences, hashes mean a named subtree. As such, the
branches are merged into one hash.

Why the “Duplicate subtrees” Message?

We now have enough rules for figuring out what the message

Duplicate subtrees while merging result of
IFTHENELSE
only the values of the latter will be kept. (keys: [:number])

means. Each of the parser atoms :_if, :_then and
:_else essentially output {:number => X}. (#2) Those
are then merged together into one hash. (#3) Since all of them have a single
key :number, parslet cannot merge them together without
discarding information. As output we get

{:number=>"3"@17}
# result of
{:number=>"1"@3}.merge({:number=>"2"@10}).merge({:number=>"3"@17})

How to fix this

To fix this, you will need to provide parslet with more information on how
to arrange the information:

As you can see, a first rule transforms all :numbers into real
Ruby numbers in one single step. A second rule then uses the semantic tags to
reorder the information in the tree into a simple triplet of [COND, THEN,
ELSE].

This shows that both levels of tags (:numbers and
:if/:then/:else) are needed and useful.

Careful readers will have noticed that the above Transformer is missing a
rule for the case where ‘else’ is missing. Here’s the full Transformer:

Due to the nature of the transformers in parslet the case distinction gets
folded into multiple pattern matching rules. This is supposed to remind
you of functional programming – Duplication is a good thing, at least here.

Wrapping it up

I’ve tried to show you some of the more technical aspects of parsing and
transformation. We’ve seen how parser output gets mangled to throw away
all those strings parslet deems unimportant to you. With the three rules given
above, you will be able to reason about this stage.

Also, I hope to inspire a ‘more is more’ approach in connection with
transformer rules. Rule actions should not handle case distinctions; a
properly encoded tree will contain enough variety to be able to match
different cases directly. It goes without saying that the code in these blocks
should be kept short, since some of it will be repeated.