Scopes
In Dauug|36 assembly language, a scope is what some languages call a subroutine or function. I decided that “scope” was a more accurate term for this architecture, because the only thing the assembler enforces about scopes is register and label namespacing.
Two example scopes
Suppose we’d like a subroutine to convert from Centigrade to Fahrenheit, and another to convert from Fahrenheit to Centigrade. To keep this example simple, we’ll assume that approximate results are sufficient. (We’re using integer arithmetic, shifts used for division round toward negative infinity, and we’re not trying to squeeze out all the precision we might.) Here’s how we may write these:
Scope to convert Centigrade to Fahrenheit
C_to_F:: signed C_in F_out ; input and output registers keep C_in F_out signed t u ; temporary registers t = C_in asr 434343_434343`o ; t = C_in / 2 u = t asr 434343_434343`o ; u = t / 2 t = t + u ; t = 0.75 * C_in u = t asr 404040_404040`o ; u = t / 16 t = t + u ; t = 0.797 * C_in t = C_in + t ; t = 1.797 * C_in F_out = t + 32 ; F_out = 1.797 * C_in + 32 return
The first thing to note is that scopes are declared using two colons after their name, so C_to_F::
introduces a scope named C_to_F
. The C_to_F
scope continues until a new scope is declared. Scopes must be contiguous, so we can’t abandon the C_to_F
scope and return to it later. Scopes do not nest.
The registers declared, C_in
and F_out
, are accessible within the C_to_F
scope by just using their names in the ordinary way. But in this scope, C_in
and F_out
are intended as input and output registers for a subroutine to convert Centigrade to Fahrenheit, so C_in
will need to be written before the scope is CALL
ed, and F_out
and will need to be read after the scope RETURN
s. We accomplish this in two stages:
- We use the
keep
keyword to make the registers accessible outside the scope. - Outside the scope, we prefix register names with scope names, i.e.,
C_to_F::C_in
.
So to call the C_to_F
scope from another scope, we might write something like:
C_to_F::C_in = temp (Centigrade) call C_to_F temp (Fahrenheit) = C_to_F::F_out
In assembler syntax, (Centigrade)
and (Fahrenheit)
are comments, because they are parenthesized.
The right arithmetic shifts are tricky to read, because Dauug|36 are always specified in terms of a left rotation. Using 43`o
and 40`o
in every tribble of the right operand indicates a right shift of 1 and 4 bits respectively.
Next, we look at the opposite conversion from Fahrenheit to Centigrade.
Scope to convert Fahrenheit to Centigrade
F_to_C:: signed F_in C_out ; input and output registers keep F_in C_out signed t u ; temporary registers C_out = F_in - 32 ; C_out = F_in - 32 u = C_out asr 414141_414141`o ; u = (F_in - 32) / 8 u = C_out - u ; u = (F_in - 32) * 7 / 8 t = u asr 363636_363636`o ; t = (F_in - 32) * 7 / 512 t = t + u ; t = (F_in - 32) * 455 / 512 t = t asr 404040_404040`o ; t = (F_in - 32) * 455 / 8192 u = C_out asr 434343_434343`o ; u = (F_in - 32) / 2 C_out = t + u ; C_out = (F_in - 32) * 4551 / 8192 return ; C_out = 0.555542 * (F_in - 32)
Here, the tribbles 43`o
, 41`o
, 40`o
, and 36`o
cause right shifts of 1, 3, 4, and 6 bits.
As mentioned, these conversions are approximate. When temperatures between −100 °F and 100 °F are converted to °C and back to °F, the output will be 0 to 4 °F lower than the input. Temperatures between −100 °C and 100 °C, when converted to °F and back to °C, will see the output 0 to 2 °C lower than the input.
More about scopes
Two registers that aren’t in the same scope are different registers, even if they have the same name. The same is true of labels (JUMP
destinations), so when deciding what to name things, you only need to consider conflicts that are in the current scope.
Because scopes begin at their ::
declaration and continue until changed, I needed to decide what happens prior to the first scope declaration in a program. I decided this is also a scope, and that its name is main
. Programs start running at the first instruction in their main scope. It’s not legal to declare scope main
—you can’t write
main::
because the assembler does that internally for you on “line 0” of your program.
Because scopes are zero-cost abstractions that create no instructions on their own, I couldn’t call them subroutines, because:
- They might not contain any code at all.
- They don’t require
CALL
to enter. - They don’t require
RETURN
orREVERT
to exit. - They don’t initialize any registers or check if you initialized them.
I also couldn’t call scopes functions for the above reasons, not to mention that they aren’t guaranteed to have inputs or outputs.
“Rolling” in and out of scopes
It’s possible to drift in and out of scopes as the instruction pointer increments. There is no inherent requirement for CALL
to reach the top of a scope, and the assembler will not require a RETURN
or REVERT
to appear at the end of a scope. This can lead to bugs if the programmer isn’t paying attention, or can be put to good use. One use for “rolling” into or out of a scope is that a subroutine with multiple entry points can be written.
As an example, suppose you’re writing a privileged routine to set the multitasking timer’s LFSR setpoint. In most applications, the setpoint would be 1010111111110111`b
, because it sets the timer for its longest possible duration. So you’d like to have that as a default value, but you’d also like to support an alternative where the caller can use any setpoint. One approach is to abut two scopes like this:
init.multitasking.timer:: init.multitasking.timer.to::setpoint = 1010111111110111`b init.multitasking.timer.to:: unsigned serial.io ; value being shifted to ff tims unsigned setpoint ; eventual setting for ff tims keep setpoint serial.io = 10_0000000000000000`b serial.io = serial.io | setpoint loop: timer serial.io jump >= loop return
The scope set.multitasking.timer.to
exposes local register setpoint
as an input, allowing you to set ff tims
to any value you want. For example, you may choose to have the timer expire after 20 instructions. You would call this as
init.multitasking.timer.to::setpoint = 1010111111110111`b call init.multitasking.timer.to
On the other hand, if you want to use the timer default of 65,535 instructions, you would instead
call init.multitasking.timer
without the .to
suffix, and without specifying setpoint
yourself. The called scope would fill in the default value for you, then “roll into” init.multitasking.timer.to
to do the work. There is only one RETURN
at the end, and it covers both scopes.
Useless scopes
The assembler does not require that scopes have any contents. Here is an example of a scope with no contents, called colour
, followed by scope color
.
colour:: color:: unsigned red green blue keep red green blue ; etc.
Although your British friends can
call colour
and be happy, they cannot access registers as
colour::green = 255
because green
is in scope color
. Scope colour
doesn’t have registers.
Register-only scopes
It can be advantageous to have scopes that contain only registers, such as for global variables or to group registers for some purpose. For instance:
g(lobal):: unsigned debug.level default.units signed timezone.seconds keep debug.level default.units timezone.seconds
Elsewhere in the program, prefix these registers’ names with g::
to use them.
Note
Scope g
doesn’t initialize these registers. You may wish to have it do so:
g(lobal):: unsigned debug.level default.units signed timezone.seconds keep debug.level default.units timezone.seconds debug.level = 0 timezone.seconds = ~14_400 default.units = 86_400 return
Your program would need to call g
to preset these values.