Code Planting Tutorial Part 3 - GetPoplog/Seed GitHub Wiki
In this third part, we take a look at how to write conditional code. The underlying model is rather like BASIC or even assembler, where you plant labels in the code-stream and arrange to jump to those labels conditionally or unconditionally. Note that although this is enough to enable you to write loops, there are some extra looping gadgets that are introduced later on, so look out for those later.
In this tutorial it helps to know that:
- The built-in function
dest
takes a linked list and returns both the head and tail. - A common idiom for stepping down a list is
lvars item = mylist.dest -> mylist;
Let's look at how we model an if E then S1 else S2 endif
expression. We will need to:
- allocate new labels using
sysNEW_LABEL()
- place them in the code stream with
sysLABEL
- conditionally branch using
sysIFNOT(label)
, which jumps tolabel
if the top of the stack isfalse
- and unconditionally branch to a label using
sysGOTO(label)
Putting these together, we will have code a bit like this. And to temporarily draw a veil over the representation of expressions, we will just assume that plantCodeFor
is a function that generates code for expressions such as E, S1 and S2. I have added pseudo-assembler comments on the right hand side that hopefully clarify what is happening on the left.
lvars else_label = sysNEW_LABEL();
lvars endif_label = sysNEW_LABEL();
plantCodeFor( E ); ;;; E
sysIFNOT( else_label ); ;;; IFNOT goto else_label
plantCodeFor( S1 ); ;;; S1
sysGOTO( endif_label ); ;;; GOTO endif_label
sysLABEL( else_label ); ;;; else_label:
plantCodeFor( S2 ); ;;; S2
sysLABEL( endif_label ); ;;; endif_label:
It is worth noticing that if this was just an if E then S endif
expression then it would be a bit simpler:
lvars endif_label = sysNEW_LABEL();
plantCodeFor( E ); ;;; E
sysIFNOT( endif_label ); ;;; IFNOT goto endif_label
plantCodeFor( S ); ;;; S1
sysLABEL( endif_label ); ;;; endif_label:
We can try to capture this by writing a plantIfThenElse
function, which suppresses the 'else' part if S2
is false
.
define plantIfThenElse( E, S1, S2 ); lvars E, S1, S2;
lvars endif_label = sysNEW_LABEL();
lvars else_label = S2 and sysNEW_LABEL() or endif_label;
plantCodeFor( E ); ;;; E
sysIFNOT( else_label ); ;;; IFNOT goto else_label
plantCodeFor( S1 ); ;;; S1
if S2 then ;;;
sysGOTO( endif_label ); ;;; GOTO endif_label
sysLABEL( else_label ); ;;; else_label:
plantCodeFor( S2 ); ;;; S2
endif; ;;;
sysLABEL( endif_label ); ;;; endif_label:
enddefine;
LISP can express a chain of if-then-else's rather neatly using COND, which takes an arbitrary number of if-then pairs and an optional-else part. It chains along the if-then pairs and if none of them are true executes the optional-else (or does nothing). Let's see how we can implement that efficiently.
The basic pattern for (COND E1 S1 E2 S2 ... S_ELSE)
looks like this:
E1
IFNOT goto E2_label
S1
GOTO endif_label
E2_label:
E2
IFNOT goto E3_label
S2
GOTO endif_label
E3_label:
S_ELSE
endif_label:
And let's put this into code. It's a simple enough pattern. There's an idiomatic use of dest
that returns both the head and tail of a list to walk the cond_list
in pairs, carefully managing the else case.
define plantCondList( cond_list ); lvars cond_list;
lvars endif_label = sysNEW_LABEL();
until null( cond_list ) do
lvars Ei = cond_list.dest -> cond_list;
if null( cond_list ) then
;;; Ei is S_ELSE
plantCodeFor( Ei );
else
lvars Si = cond_list.dest -> cond_list;
plantCodeFor( Ei );
lvars next_label = sysNEW_LABEL();
sysIFNOT( next_label );
plantCodeFor( Si );
sysGOTO( endif_label );
sysLABEL( next_label );
endif
enduntil;
sysLABEL( endif_label );
enddefine;
Code-planting code can easily get complicated and difficult to read. It's very common to want to inspect the stream of code that is being emitted. To do that load the library LIB SHOWCODE.
lib showcode
and then modify plantCondList to locally set the variable pop_show_code
to true.
define plantCondList( cond_list ); lvars cond_list;
dlocal pop_show_code = true;
... rest of the code as before ...
enddefine;
Now we will get a trace of the code stream. To see this in action we will give the placeholder plantCodeFor
function a simple definition. It will just push a constant onto the stack (sysPUSHQ
).
define plantCodeFor( E ); lvars E;
sysPUSHQ( E )
enddefine;
So now we can try a sequence such as [ ^false 1 ^false 2 ^true 3 4 ], which ought to have the same meaning as:
if false then 1
elseif false then 2
elseif true then 3
else 4
endif
Let's try it out:
: vars procedure f1f2t34 = buildNewProcedure( plantCondList(% [ ^false 1 ^false 2 ^true 3 4 ] %) );
PUSHQ <false>
IFNOT label_20
PUSHQ 1
GOTO label_19
label_20:
PUSHQ <false>
IFNOT label_21
PUSHQ 2
GOTO label_19
label_21:
PUSHQ <true>
IFNOT label_22
PUSHQ 3
GOTO label_19
label_22:
PUSHQ 4
label_19:
: f1f2t34() =>
** 3
:
Nice! Now you may be asking whether or not you can see the code generated by the Pop-11 interpreter too? Sure, we just invoke the Pop-11 compiler on an input stream - in this case drawn from a string.
: vars some_code = 'if false then 1 elseif false then 2 elseif true then 3 else 4 endif';
: procedure(); dlocal pop_show_code = true; some_code.stringin.pop11_compile endprocedure() =>
COMPILE <procedure pop11_exec_stmnt_seq_to>
PUSH false
IFNOT label_28
PUSHQ 1
GOTO label_27
label_28:
PUSH false
IFNOT label_29
PUSHQ 2
GOTO label_27
label_29:
PUSH true
IFNOT label_30
PUSHQ 3
GOTO label_27
label_30:
PUSHQ 4
label_27:
EXECUTE
** 3
:
Which, unsurprisingly, looks just like the code emitted by our plantCondList
.