## “Finite Field Arithmetic.” Chapter 4: Interlude: FFACalc.

*This article is part of a series of hands-on tutorials introducing FFA, or the Finite Field Arithmetic library. FFA differs from the typical “Open Sores” abomination, in that — rather than trusting the author blindly with their lives — prospective users are expected to read and fully understand every single line. In exactly the same manner that you would understand and pack your own parachute. The reader will assemble and test a working FFA with his own hands, and at the same time grasp the purpose of each moving part therein.*

- Chapter 1: Genesis.
- Chapter 2: Logical and Bitwise Operations.
- Chapter 3: Shifts.
**Chapter 4: Interlude: FFACalc.**

You will need:

**All**of the materials from Chapters 1, 2, and 3.- ffa_ch4_ffacalc.vpatch
- ffa_ch4_ffacalc.vpatch.asciilifeform.sig

Add the above *vpatch* and *seal* to your V-set, and *press* to *ch4_ffacalc.vpatch.*

Just like before, you will end up with two directories, *libffa* and *ffademo*.

However you will also see a new one, **ffacalc**.

Now compile *ffacalc*:

cd ffacalc gprbuild

But **do not run it quite yet.**

As the title of this chapter suggests, it will not introduce fundamentally new *FFA* material. Instead, you will meet *FFACalc* — a program which makes practical use of the routines presented in Chapters 1, 2, and 3. Henceforth every Chapter in this series will build on *FFACalc*, rather than continuing to expand the rather-uninteresting *ffademo*. When we reach the final Chapter, the reader will notice that… but let’s not spoil it!

For now, *FFACalc* is exactly what the name implies: a *FFAtronic* RPN calculator, capable strictly of addition, subtraction, basic bitwise ops, numeric comparison, a small number of simple stack manipulations (a la *Forth*), and some elementary I/O.

But first, a few small helper-function additions to *libFFA*.

Calculators need to accept numeric input, and it is to be processed one hexadecimal digit at a time. Therefore a nibble subtype of *Word* is called for:

*words.ads*:

-- Word, restricted to Nibble range. subtype Nibble is Word range 0 .. 16#F#;

Predicate operators produce strictly *WBool* (see Chapter 1) outputs. *FFACalc* will operate strictly in *FZ* integers. Therefore a conversion is required:

*fz_basic.ads*:

-- Set given FZ to a given truth value procedure WBool_To_FZ(V : in WBool; N : out FZ);

*fz_basic.adb*:

-- Set given FZ to a given truth value procedure WBool_To_FZ(V : in WBool; N : out FZ) is begin FZ_Clear(N); FZ_Set_Head(N, V); end WBool_To_FZ; pragma Inline_Always(WBool_To_FZ);

Sometimes, we will need to go in the other direction, and produce a *WBool* from an *FZ*, based on the Boolean meaning of its value (i.e. whether it is a nonzero.) This will look like this:

*w_pred.ads*:

-- Return 1 if N is unequal to 0; otherwise return 0. function W_NZeroP(N : in Word) return WBool;

*w_pred.adb*:

-- Return 1 if N is unequal to 0; otherwise return 0. function W_NZeroP(N : in Word) return WBool is begin return 1 xor W_ZeroP(N); end W_NZeroP; pragma Inline_Always(W_NZeroP);

Now since we are introducing a program where the user is able to control the width of an instantiated *FFAtron*, we will need a validity predicate. FFA “bitness” is not an arbitrary positive number, but *must* be an integer multiple of all historical machine word sizes, and additionally a power of two (the reason for the latter restriction will become apparent in the Sub-Quadratic Multiplication Chapter.) And so:

*fz_lim.ads*:

package FZ_Lim is pragma Pure; FZ_Minimal_Bitness : constant Positive := 256; FZ_Validity_Rule_Doc : constant String := "Must be greater than or equal to 256, and a power of 2."; -- Determine if a proposed FFA Bitness is valid. function FZ_Valid_Bitness_P(B : in Positive) return Boolean; end FZ_Lim;

*fz_lim.adb*:

package body FZ_Lim is -- Determine if a proposed FFA Bitness is valid. function FZ_Valid_Bitness_P(B : in Positive) return Boolean is Result : Boolean := False; T : Natural := B; PopCount : Natural := 0; begin -- Supposing we meet the minimal bitness: if B >= FZ_Minimal_Bitness then while T > 0 loop if T mod 2 = 1 then PopCount := PopCount + 1; end if; T := T / 2; end loop; -- Is B a power of 2? if PopCount = 1 then Result := True; end if; end if; return Result; end FZ_Valid_Bitness_P; end FZ_Lim;

In a power of 2, the “popcount” (number of 1s) is necessarily 1. The mechanics of this routine should be apparent to the alert reader, nothing more will be said of it.

And that’s all for *LibFFA*, for the time being. Now returning to *FFACalc*: we will shed the old *ffademo*’s dependence on Ada’s standard I/O library, in favour of a more compact approach:

*os.ads*:

with Interfaces; use Interfaces; with Interfaces.C; use Interfaces.C; package OS is -- Receive a character from the TTY, and True if success (False if EOF) function Read_Char(C : out Character) return Boolean; -- Send a character to the TTY. procedure Write_Char(C : in Character); -- Send a Newline to the TTY. procedure Write_Newline; -- Exit with an error condition report. procedure Eggog(M : String); procedure Quit(Return_Code : Integer); pragma Import (Convention => C, Entity => Quit, External_Name => "exit"); private -- POSIX stdio: EOF : constant int := -1; function GetChar return int; pragma Import(C, getchar); function PutChar(item: int) return int; pragma Import(C, putchar); -- GNATistic procedure To_Stderr(C : Character); pragma Import(Ada, To_Stderr, "__gnat_to_stderr_char"); Sadness_Code : constant Integer := -1; end OS;

*os.adb*:

package body OS is -- Receive a character from the TTY, and True if success (False if EOF) function Read_Char(C : out Character) return Boolean is i : int; Result : Boolean := False; begin i := GetChar; if i /= EOF then C := Character'Val(i); Result := True; end if; return Result; end Read_Char; -- Send a character to the TTY. procedure Write_Char(C : in Character) is R : int; pragma Unreferenced(R); begin R := PutChar(int(Character'Pos(C))); end Write_Char; -- Send a Newline to the TTY. procedure Write_Newline is begin Write_Char(Character'Val(16#A#)); end Write_Newline; -- Exit with an error condition report. procedure Eggog(M : String) is begin for i in 1 .. M'Length loop To_Stderr(M(I)); end loop; -- Emit LF To_Stderr(Character'Val(16#A#)); -- Exit Quit(Sadness_Code); end; end OS;

Now for a bit of surprise. Did you know that it is impossible to make use of the standard *Ada.Command_Line* functionality in a program where the

pragma Restrictions(No_Secondary_Stack);

… restriction is in force?

The reason for this becomes apparent in a careful reading of the Standard, where we find the following turd:

function Argument (Number : in Positive) return String;

Indeed, a *completely* unnecessary invocation of the secondary stack ! Why did the authors of the Standard do this ? I have no idea, but we will have to correct their mistake! Unfortunately it is quite impossible to do this without invoking some GNATisms. And so, we must:

*cmdline.ads*:

with System; package CmdLine is -- IMHO this is reasonable. CmdLineArg_Length : constant Positive := 256; subtype CmdLineArg is String(1 .. CmdLineArg_Length); function Initialized return Boolean; function Arg_Count return Natural; pragma Import(C, Arg_Count, "__gnat_arg_count"); procedure Get_Argument(Number : in Natural; Result : out String); private procedure Fill_Arg (A : System.Address; Arg_Num : Integer); pragma Import(C, Fill_Arg, "__gnat_fill_arg"); function Len_Arg (Arg_Num : Integer) return Integer; pragma Import(C, Len_Arg, "__gnat_len_arg"); end CmdLine;

*cmdline.adb*:

with System; use System; package body CmdLine is -- Test if GNAT's cmdline mechanism is available function Initialized return Boolean is gnat_argv : System.Address; pragma Import (C, gnat_argv, "gnat_argv"); begin return gnat_argv /= System.Null_Address; end Initialized; -- Fill the provided string with the text of Number-th cmdline arg procedure Get_Argument(Number : in Natural; Result : out String) is begin if Number >= Arg_Count or (not Initialized) then raise Constraint_Error; end if; declare L : constant Integer := Len_Arg(Number); Arg : aliased String(1 .. L); begin -- Will it fit into the available space? if L > Result'Length then raise Constraint_Error; end if; -- Get this arg string from where GNAT stowed it Fill_Arg(Arg'Address, Number); -- Copy it to Result: Result := (others => ' '); Result(Arg'Range) := Arg; end; end Get_Argument; end CmdLine;

How this is invoked, will soon become quite apparent. Let’s at last proceed to *FFACalc* !

We make use here of nearly everything we have seen in Chapters 1-3:

*ffa_calc.adb*:

-- Basics with OS; use OS; with CmdLine; use CmdLine; -- FFA with FZ_Lim; use FZ_Lim; with Words; use Words; with W_Pred; use W_Pred; with FZ_Type; use FZ_Type; with FZ_Basic; use FZ_Basic; with FZ_Arith; use FZ_Arith; with FZ_Cmp; use FZ_Cmp; with FZ_Pred; use FZ_Pred; with FZ_BitOp; use FZ_BitOp; with FZ_Shift; use FZ_Shift; -- For Output with FFA_IO; use FFA_IO;

procedure FFA_Calc is Width : Positive; -- Desired FFA Width Height : Positive; -- Desired Height of Stack begin if Arg_Count /= 3 then Eggog("Usage: ./ffa_calc WIDTH HEIGHT"); end if; declare Arg1 : CmdLineArg; Arg2 : CmdLineArg; begin -- Get commandline args: Get_Argument(1, Arg1); -- First arg Get_Argument(2, Arg2); -- Second arg -- Parse into Positives: Width := Positive'Value(Arg1); Height := Positive'Value(Arg2); exception when others => Eggog("Invalid arguments!"); end; -- Test if proposed Width is permissible: if not FZ_Valid_Bitness_P(Width) then Eggog("Invalid Width: " & FZ_Validity_Rule_Doc); end if;

Above, we see how our replacement for Ada’s standard command-line argument reader works. Instead of demanding the secondary stack, we make use of pre-allocated strings, into which each argument is copied. The reader is invited to try and overflow these: the resulting death is a clean one.

Now for the calculator…

-- The Calculator itself: declare -- The number of Words required to make a FZ of the given Bitness. Wordness : Indices := Indices(Width / Bitness); -------------------------------------------------------- -- State -- -------------------------------------------------------- -- The Stack: subtype Stack_Positions is Natural range 0 .. Height; type Stacks is array(Stack_Positions range <>) of FZ(1 .. Wordness); Stack : Stacks(Stack_Positions'Range); -- Stack Pointer: SP : Stack_Positions := Stack_Positions'First; -- Carry/Borrow Flag: Flag : WBool := 0; -- Odometer: Pos : Natural := 0; -- The current levels of the three types of nestedness: QuoteLevel : Natural := 0; CommLevel : Natural := 0; CondLevel : Natural := 0; --------------------------------------------------------

Observe that the FORTH-like stack is allocated on the stack (your machine’s, that is), and its height is determined by the *HEIGHT* parameter given in the second command line argument. The width of the *FZ* integers comprising the elements of this stack, is in turn given by *WIDTH*, the first command line argument.

Now for some elementary stack-manipulation routines:

-- Clear the stack and set SP to bottom. procedure Zap is begin -- Clear the stack for i in Stack'Range loop FZ_Clear(Stack(i)); end loop; -- Set SP to bottom SP := Stack_Positions'First; -- Clear Overflow flag Flag := 0; end Zap; -- Report a fatal error condition at the current symbol procedure E(S : in String) is begin Eggog("Pos:" & Natural'Image(Pos) & ": " & S); end E; -- Move SP up procedure Push is begin if SP = Stack_Positions'Last then E("Stack Overflow!"); else SP := SP + 1; end if; end Push; -- Discard the top of the stack procedure Drop is begin FZ_Clear(Stack(SP)); SP := SP - 1; end Drop; -- Check if stack has the necessary N items procedure Want(N : in Positive) is begin if SP < N then E("Stack Underflow!"); end if; end Want;

Here we make use of the FZ_ShiftLeft operation we implemented in Chapter 3:

-- Slide a new hex digit into the FZ on top of stack procedure Ins_Hex_Digit(N : in out FZ; D : in Nibble) is Overflow : Word := 0; begin -- Make room in this FZ for one additional hex digit FZ_ShiftLeft_O(N => N, ShiftedN => N, Count => 4, Overflow => Overflow); -- Constants which exceed the Width are forbidden: if W_NZeroP(Overflow) = 1 then E("Constant Exceeds Bitness!"); end if; -- Set the new digit FZ_Or_W(N, D); end;

And now for the "opcodes" comprising our stack machine:

-- Execute a Normal Op procedure Op_Normal(C : in Character) is -- Over/underflow output from certain ops F : Word; begin case C is -------------- -- Stickies -- -------------- -- Enter Commented when '(' => CommLevel := 1; -- Exit Commented (but we aren't in it!) when ')' => E("Mismatched close-comment parenthesis !"); -- Enter Quoted when '[' => QuoteLevel := 1; -- Exit Quoted (but we aren't in it!) when ']' => E("Mismatched close-quote bracket !"); -- Enter a ~taken~ Conditional branch: when '{' => Want(1); if FZ_ZeroP(Stack(SP)) = 1 then CondLevel := 1; end if; Drop; -- Exit from a ~non-taken~ Conditional branch: -- ... we push a 0, to suppress the 'else' clause when '}' => Push; WBool_To_FZ(0, Stack(SP)); ---------------- -- Immediates -- ---------------- -- These operate on the FZ ~currently~ at top of the stack; -- and this means that the stack may NOT be empty. when '0' .. '9' => Want(1); Ins_Hex_Digit(Stack(SP), Character'Pos(C) - Character'Pos('0')); when 'A' .. 'F' => Want(1); Ins_Hex_Digit(Stack(SP), 10 + Character'Pos(C) - Character'Pos('A')); when 'a' .. 'f' => Want(1); Ins_Hex_Digit(Stack(SP), 10 + Character'Pos(C) - Character'Pos('a')); ------------------ -- Stack Motion -- ------------------ -- Push a 0 onto the stack when '.' => Push; FZ_Clear(Stack(SP)); -- Dup when '″' => Want(1); Push; Stack(SP) := Stack(SP - 1); -- Drop when '_' => Want(1); Drop; -- Swap when ''' => Want(2); FZ_Swap(Stack(SP), Stack(SP - 1)); -- Over when '`' => Want(2); Push; Stack(SP) := Stack(SP - 2); ---------------- -- Predicates -- ---------------- -- Equality when '=' => Want(2); WBool_To_FZ(FZ_Eqp(X => Stack(SP), Y => Stack(SP - 1)), Stack(SP - 1)); Drop; -- Less-Than when '< ' => Want(2); WBool_To_FZ(FZ_LessThanP(X => Stack(SP - 1), Y => Stack(SP)), Stack(SP - 1)); Drop; -- Greater-Than when '>' => Want(2); WBool_To_FZ(FZ_GreaterThanP(X => Stack(SP - 1), Y => Stack(SP)), Stack(SP - 1)); Drop; ---------------- -- Arithmetic -- ---------------- -- Subtract when '-' => Want(2); FZ_Sub(X => Stack(SP - 1), Y => Stack(SP), Difference => Stack(SP - 1), Underflow => F); Flag := W_NZeroP(F); Drop; -- Add when '+' => Want(2); FZ_Add(X => Stack(SP - 1), Y => Stack(SP), Sum => Stack(SP - 1), Overflow => F); Flag := W_NZeroP(F); Drop; ----------------- -- Bitwise Ops -- ----------------- -- Bitwise-And when '&' => Want(2); FZ_And(X => Stack(SP - 1), Y => Stack(SP), Result => Stack(SP - 1)); Drop; -- Bitwise-Or when '|' => Want(2); FZ_Or(X => Stack(SP - 1), Y => Stack(SP), Result => Stack(SP - 1)); Drop; -- Bitwise-Xor when '^' => Want(2); FZ_Xor(X => Stack(SP - 1), Y => Stack(SP), Result => Stack(SP - 1)); Drop; -- Bitwise-Not (1s-Complement) when '~' => Want(1); FZ_Not(Stack(SP), Stack(SP)); ----------- -- Other -- ----------- -- mUx when 'U' => Want(3); FZ_Mux(X => Stack(SP - 2), Y => Stack(SP - 1), Result => Stack(SP - 2), Sel => FZ_NZeroP(Stack(SP))); Drop; Drop; -- Put the Overflow flag on the stack when 'O' => Push; WBool_To_FZ(Flag, Stack(SP)); -- Print the FZ on the top of the stack when '#' => Want(1); Dump(Stack(SP)); Drop; -- Zap (reset) when 'Z' => Zap; -- Quit with Stack Trace when 'Q' => for I in reverse Stack'First + 1 .. SP loop Dump(Stack(I)); end loop; Quit(0); ---------- -- NOPs -- ---------- -- Ops we have not yet spoken of -- do nothing when others => null; end case; end Op_Normal; -- Process a Symbol procedure Op(C : in Character) is begin -- First, see whether we are in a state of nestedness: -- ... in a Comment block: if CommLevel > 0 then case C is when ')' => -- Drop a nesting level: CommLevel := CommLevel - 1; when '(' => -- Add a nesting level: CommLevel := CommLevel + 1; when others => null; -- Other symbols have no effect at all end case; -- ... in a Quote block: elsif QuoteLevel > 0 then case C is when ']' => -- Drop a nesting level: QuoteLevel := QuoteLevel - 1; when '[' => -- Add a nesting level: QuoteLevel := QuoteLevel + 1; when others => null; -- Other symbols have no effect on the level end case; -- If we aren't the mode-exiting ']', print current symbol: if QuoteLevel > 0 then Write_Char(C); end if; --- ... in a ~taken~ Conditional branch: elsif CondLevel > 0 then case C is when '}' => -- Drop a nesting level: CondLevel := CondLevel - 1; -- If we exited the Conditional as a result, -- we push a 1 to trigger the possible 'else' clause: if CondLevel = 0 then Push; WBool_To_FZ(1, Stack(SP)); end if; when '{' => -- Add a nesting level: CondLevel := CondLevel + 1; when others => null; -- Other symbols have no effect on the level end case; else -- This is a Normal Op, so proceed with the normal rules. Op_Normal(C); end if; end Op; -- Current Character C : Character; begin -- Reset the Calculator Zap; -- Process characters until EOF: loop if Read_Char(C) then -- Execute Op: Op(C); -- Advance Odometer Pos := Pos + 1; else Zap; Quit(0); -- if EOF, we're done end if; end loop; end; end FFA_Calc;

But rather than describing in detail the operation of *FFACalc*, I will invite the reader to build it, and solve the following puzzle:

Write a *FFACalc tape* that will take seven numbers, presumed to be on the top of the stack, and return the largest.

Your answer should work with *any* legal *WIDTH*, and any stack *HEIGHT* large enough to hold the working set.

For instance, suppose file *numbers.txt* were to contain:

.9.1.7.5.1.1.0

Then the following example invocation:

cd ffacalc gprbuild cat numbers.txt youranswer.txt | ./bin/ffa_calc 256 16

... should produce the output:

0000000000000000000000000000000000000000000000000000000000000009

... and similarly for any other seven numbers.

A solution will be posted in the next Chapter.

*~To be continued!~*

That’s splitting hairs, but

`if T mod 2 = 1 then`

PopCount := PopCount + 1;

end if;

could be

PopCount := PopCount + T mod 2;

or even just check that

`(T and (T - 1)) = 0`

(might be too clever, and needs type conversion)And why Ins_Hex_Digit doesn’t use FZ_ShiftLeft_O_I ? Seems tailor-made for that.

Dear apeloyee,

Re: popcount: good point, and I’ll put this in.

Re: the digits: using FZ_ShiftLeft_O_I would require first shifting the nibble so that it sits at the top of a word, which imho is ugly.

Yours,

-S

first posted elsewhere, the sha1 of my solution to the puzzle is b579b2c553ee2bd3aee8d17d96ae259abfad2ac5

i’ll post the complete solution here, once next chapter is released!

`subtype Stack_Positions is Natural range 0 .. Height;`

type Stacks is array(Stack_Positions range ) of FZ(1 .. Wordness);

Stack : Stacks(Stack_Positions'Range);

This is somewhat tricky. From the rest of the code, the Stack(0) is never used. Why not declare it as

`Stack : Stacks(Stack_Positions'First +1 .. Stack_Positions'Last );`

or preferably simply

`Stack : Stacks(1 .. Height );`

(given as |Want| already assumes indices start from 1, rather than using Stack’First)?Dear apeloyee,

This won’t work, think about why:

Stack(SP)must be a valid reference at all times, even when the stack is empty, or the program will barf at elaboration.We’re stuck with the ‘wasted’ FZ at the bottom of the stack.

Yours,

-S

Can you cite the clause of the Adastandard requiring that?

It does seem to build and work with this change. (Of course, the code for the ‘Q’ command needs to be changed also).

Dear Apeloyee,

My observation came from my original experiments, where instead of the ugly repeating Stack(SP – 1), etc. references I had a “rename” clause at the head of the proggy; this croaked, as Stack(SP – 1), – 2, etc are invalid when SP is already at rock-bottom, even if the “rename” var is not referenced at any time afterwards in the scope.

I have not yet found where in the Standard this behaviour is specified.

Please consider posting your version of the routine ? If it works, and I can justify that it always must work (rather than accidentally) — I will put it into use and cite you in the commentary.

Yours,

-S

> I had a “rename” clause at the head of the proggy; this croaked, as Stack(SP – 1), – 2, etc are invalid when SP is already at rock-bottom, even if the “rename” var is not referenced at any time afterwards in the scope.

I didn’t know such a version existed. Not surprising, then.

> Please consider posting your version of the routine ? If it works, and I can justify that it always must work (rather than accidentally) — I will put it into use and cite you in the commentary.

http://p.bvulpes.com/pastes/BDpG6/?raw=true

Corresponds to present ch. 6, with two differences mentioned. In any case, Stack(0) never needs to be accessed, and if forbidding this access doesn’t work, it means original version has a bug.

For the time when the paste vanishes:

`@@ -82,7 +82,7 @@`

-- The Stack:

subtype Stack_Positions is Natural range 0 .. Height;

type Stacks is array(Stack_Positions range ) of FZ(1 .. Wordness);

- Stack : Stacks(Stack_Positions'Range);

+ Stack : Stacks(1..Height);

-- Stack Pointer:

SP : Stack_Positions := Stack_Positions'First;

@@ -449,7 +449,7 @@

`-- Quit with Stack Trace`

when 'Q' =>

- for I in reverse Stack'First + 1 .. SP loop

+ for I in reverse Stack'First .. SP loop

Dump(Stack(I));

end loop;

Quit(0);

So?

Dear apeloyee,

I have not forgotten about this item; it is on the conveyor.

Yours,

-S

Anyway, my sig for the patch, ffa_ch4_ffacalc.vpatch.peterl.sig

-----BEGIN PGP SIGNATURE-----

Version: GnuPG v1

iQIcBAABAgAGBQJaQxNbAAoJELTi4Nystj0lRFYP/3BzzsKZDSNXIqvl45TjP0rK

z8COLZgL9jwV6dHyRY9RqmBkGVpW3XQbfDwvfI4JtOeJ0kZOOt2eixuFthE23jrg

V9bsLInjaT6cDvUh4xc3zkHxPan5xGfRb2riUysMrCp9lQysU7qyIEGqVEwhxBUV

JKwC6AnCHmaW5bmeCwVQmGg0ujFh4KmhYzztDxYnliiWlm7fqCYYzrnCB43MUdeU

kcIbGxhZaN9PAM6JqWDLlybvewktyCfTACxSZbmamcE/XHk8VV28ziKzNZ72cqVZ

YVbrgIoDXmqNwdlVUpUufnHyVdFkF8nSZ0folzj7llMjoeYOzJQJuxUuELSz8883

5tRGTq3CT5w5JdDEJGjJYMjQXRGfVt0fZawRuZWCOl+JePl9fkVtRfwgayTco0Y7

NQ4DftzndItMwze1ZAeA3afSl1uUM7nysc7M6jGtxkYSFnLXAS+dQyGz58lvsjDW

0epzfH3ssPNZ9328h3QQ93tDPQFGSlKDrYP4eqngLj8bBjh9cliLicHUy8jqHzO6

uP2LgnjAoBWC3IdAie4OBxzYSOZViY/EzIRk78jSaEuEIVDkT8jCEnzNlK0n0JVA

xKTWE/fYvcxB5pVGde1FJLGT1HHau8EUSuz92K+lfEcZqHPGrtOWNekJUkJUYSFL

Fzl9e5jWJ8gM3TGZlsDQ

=Xta0

-----END PGP SIGNATURE-----

Dear PeterL,

Thanks, your sig will live here now.

Yours,

-S

Tiny nitpick: while I enjoy on a level “Nibble” as type name, wouldn’t this be more accurately named Hex_Digit or similar? Especially since it is anyway used as such in Ins_Hex_Digit so it is rather fixed as hex, isn’t it?

Dear Diana Coman,

Funnily enough I originally had it named “Hex_Digit”, and renamed to “Nibble” on account of the latter being the customary name for a quantity of bitness 4.

Yours,

-S

iQIcBAABCgAGBQJaUBQjAAoJEPAbzAMu8lJHBxsP/3OfFNzdpNx3ZKJCKBuealxM

fBS+8l9/mJM8hslygYBZZ5BqX3Cpoi5FoQP6OKwaesB8x3jibmBcC0aK3qS0J0aG

c1R7R09Xz0s5sx+5lvILFeDfYP68eeGKksYztT7DbOp3IfKwEV1W4eqsKqCo+LOa

iPpVc3hD9dl1UYVThJDWRoZgSd8dUe9gj64zoEdN8VMDYal78mrCgQoR5ksj41c1

PpiAJ1LlcVumdIyOzs8W3e1BzTn1wtOXFZIiFDPEgOJUBQ/fuYabffToMEn49tcO

6P5Uc8JuywHShOeAYdlnN5Qb/6uiWrSpc0nIIdYH9hVT5E6ilBWQP+hZwW3Z9KuA

2wbYp/BzbZpP0/fJ+nJShReds2xYK3zjmKgsUeWSpJP4JGZCM/qBXKhO7vBxouGT

io0DLLbi+OmEyhkFSAEyn0z4VoytplTRg/gq+0BhICWdQ1IbkRZOuS+tapzKvP2/

DO9qSWzL6WHoMJQh6wHLVNM5Hxp/Ri7WzbgRHd8Qr08VS/htk1TmHpSZqoUb0qKt

aq5/ZlIWqxlIKGuTpk5+Jt7XYzUVf/8nKkWyZHmlIF/IXa1MBAEBq1JKwQwfI0bu

IKe+zb1eMNJNhH+19qvwDx6hM5bCNfaic9LeMn2UaxnhuRUzy6udKkZTmb9AAuOC

adpnTDp9VKqUSgTsiYG1

=1N4B

-----END PGP SIGNATURE-----

I had a blast solving that puzzle, by the way!

Dear BenVulpes,

Congrats!

I have perma-hosted your sig here.

Yours,

-S

Dear Stan,

I had a lot of fun getting acquainted with FFACalc! I’ve posted a solution to the puzzle (along with some doodling on FFA) on my blog. Also, following Diana’s example, I’ve posted the seals to chapters 2-4 on a separate page.