Blocks¶
So far, we’ve only seen examples of very straightforward Cf0x10 programs, which execute one line after another except for a comefrom
jump. At this point, you might be thinking, “Meh, I could do that in Intercal,” so it is time to see where Cf0x10 separates itself the rest: it allows structured programming with lexical scope!
That’s right, it really is a language for the twentieth century.
Like all the good modern languages, Cf0x10 uses syntactically significant indentation. That is, indentation creates a block:
Program | Output |
---|---|
leave_this_place
"Goodbye, cruel world"
"Hello, world"
|
Hello, world
|
In the above, execution begins in the top-level scope. Nothing ever jumps into the block named leave_this_place
, so none of the code in that block executes.
For the moment, you can think of blocks as coroutines in other languages, but that’s not really accurate, as we’ll soon see.
Qualified comefrom¶
First, how to jump into and between blocks? In the previous part, we saw “conditional” comefrom, now we see “qualified” comefrom:
Program | Output |
---|---|
foo
1
comefrom bar
3
bar
comefrom foo
2
4
|
1
2
3
4
|
When qualified, as in comefrom foo
, the program only jumps to that location from yield points in the block named foo
.
Let’s break down how this works.
Execution begins at the line just after foo
, which prints 1
. Cf0x10 has no magic names like “main;” if there is no code outside of a block, execution begins at the first block in the file. Next:
- Blank line yields
- Jumps to the
bar
block, at the linecomefrom foo
- Prints
2
- Blank line yields
- Jumps back to the
foo
block, at the linecomefrom bar
- Prints
3
So far this is all pretty obvious, but now we get to the amazing part. How does 4
get printed? We just jumped back to foo
, so how do we get to bar
again?
Recall that the program jumped from the middle of the bar
to foo
. After printing 3
, the foo
block ends and execution returns from whence it came. The next line in bar
prints 4
. Isn’t structured programming great?
Notice that even though the previous jump was from foo
to bar
, execution never returned to the blank line in foo
, otherwise the program would have printed another 3
at the end. This is one way Cf0x10 blocks are significantly different, (and simpler!) than coroutines.
Comefrom ordering¶
We previously saw that multiple eligible comefrom jumps are resolved in the order they appear in the source. This only applies, however, within the a single scope. When multiple comefroms are eligible targets in separate scopes, all of them will execute.
Comefrom0x10 picks exactly one comefrom in each scope to execute, then jumps to the comefroms in a well-defined order. The comefrom in the current scope executes last, that is, after comefroms in nested scopes.
This ordering is not recursive. That is, comefroms in nested scopes execute top-down, in source code order, depth-first.
While these rules may seem complex, they are actually the most intuitive known solution to the multiple-comefrom problem [1]. An example may make this more clear:
Program | Output |
---|---|
outside
# blocks cannot start with blank line
"don't print this"
comefrom
5
inside
comefrom
"don't print this"
comefrom
1
deeper_one
comefrom
inception
comefrom
3
2
deeper_two
comefrom
4
|
1
2
3
4
5
|
When this program arrives at the first blank line in the outside
block, all comefrom
statements are eligible targets. They execute in this order:
- Second
comefrom
in theinside
block, by the last-first and top-down rules - Only
comefrom
indeeper_one
, by the first-first and top-down rules - Only
comefrom
in theinception
block, by the depth-first rule - Only
comefrom
indeeper_two
(figure out why) - Only
comefrom
in theoutside
block, by the current-scope-last rule
Notice that each block finishes executing before the program moves to the next comefrom.
Scope¶
Naturally, you’ll need to modularize your Cf0x10 programs. Block-scoping lets you do this without worrying about things like “classes” in other languages. Block scope works essentially the same way as in every other modern language, so we can put this all together into an idiomatic Cf0x10 program:
'start program'
until = 6
loops = 0
'n is ' n ' after ' loops
count
comefrom
'start counting'
n = 0
#
next
comefrom if next
next = 1/0
loops = loops + 1
n = n + 1
comefrom next if n < until
'n is ' n
next = 1
'done counting'
never_runs
comefrom next if loops
'foobar'
This is a rather complicated method of printing the numbers from 1 to 5. It prints:
start program
start counting
n is 0
n is 1
n is 2
n is 3
n is 4
n is 5
done counting
n is after 6
This is a big jump in complexity, so let’s break it down line-by-line.
The program starts at the first line, which just prints “start program”:
'start program'
Since changing until
does not affect any comefrom
statements, there are no jumps at the assignment:
until = 6
Next, the blank line yields and the program jumps to the first line in the count
block:
comefrom
Then it prints “start counting”:
'start counting'
Initializing n
to zero actually does not cause a jump, because the only comefrom
that refers to n
is qualified by comefrom next
:
n = 0
The empty comment line is for aesthetic reasons, just to separate the block declaration for next
from the lines above it:
#
Execution passes the next
block declaration. Blank lines that immediately follow blocks do not trigger jumps, so the next line that executes is a comefrom. To help write robust programs, Cf0x10 is designed to avoid side-effects as much as possible, thus, executing a comefrom
lines never does anything:
comefrom next if n < until
Now, it prints the current value of n
, that is “n is 0”:
'n is ' n
Whoa! It looks like the next line redefines next
. In fact, it does not. For easier programming, Cf0x10 doesn’t have first-class blocks, as languages like Java has shown us that most programmers don’t want or understand them. It’s quite common in Cf0x10 to use a variable with the same name as a block. In this case, we’re using it to cause a jump:
next = 1
Because next
, the variable changes from undefined
to 1
, the program continues in next
, the block:
comefrom if next
Resetting next
, the variable, to undefined
does not cause a jump, since the if next
evaluates to falsy after this assignment (assigning to zero would have worked just as well):
next = 1/0
The variable loops
is defined in the outermost scope. This does not trigger a jump into the block never_runs
because the block next
is not visible from the scope of never_runs
:
loops = loops + 1
Incrementing n
triggers a jump back into the count
block:
n = n + 1
So, count
continues at:
comefrom next if n < until
And it prints the updated value of n
, “n is 1”:
'n is ' n
Resetting next
, the variable, jumps back into next
, the block, and repeats printing incremented values of n
until 6
, when if n < until
evaluates to false. Instead of jumping, next
, the block, finishes execution. The last line executed before jumping into next
was next = 1
, so the next line to execute prints “done counting”:
'done counting'
Finally, the program returns to the line after the very first jump and prints “n is after 6”, illustrating that n
is defined only within the lexical scope of the count
block, but loops
is visible:
'n is ' n ' after ' loops
[1] | In many languages with comefrom-based constructs, multiple comefroms are simply an error. See Comefrom for more motivation. |