Commit c90218fe authored by Peter Fidelman's avatar Peter Fidelman
Browse files

Add commentary

parent 8b8f932d
# Day 1
This is a pretty simple problem. The "hardest" part is reading integers from the terminal keyboard. ANS Forth provides a word [>NUMBER](https://forth-standard.org/standard/core/toNUMBER) `( ud a n -- ud a n )`, where `ud` is a double-length accumulator that should start at zero, and `a n` denotes the char buffer to convert. I jam this together with [ACCEPT](https://forth-standard.org/standard/core/ACCEPT), and read lines into the [PAD](https://forth-standard.org/standard/core/PAD) before converting them to integers. The stack manipulation is ugly, but what are ya gonna do.
For the sliding window in part B, I don't sum its contents. It's simpler to check if the number added to the window is bigger than the number that slid off the end of the window.
# Day 2
Code is data, data is code.
A Forth program is space-delimited words. The submarine command is also space-delimited words: `forward 1`, `down 2`, and so on.
By defining `forward`, `up` and `down` as appropriate Forth words, the submarine command becomes a syntactically correct Forth program which can be executed directly from the REPL. Run the command `report` to see where the submarine wound up.
# Day 3
## Part A
The most interesting thing going on in this program is `p`, created by the defining-word `pops`.
When defined, `p` creates and zero-initializes an array of twelve words. This array will accumulate the number of times each binary digit was 1.
When executed, `p` returns the address of the current word in this array and then advances to the next word (by advancing `bit`). `bit` gets reset to zero in `do-cr` at the end of each line of input.
If the program sees two carriage returns in a row (meaning a blank line), it stops reading input, returns from `go` and moves on to calculating gamma and delta.
## Part B
This is a significant leap in difficulty over part A. Instead of a single pass of ranking, there are now up to twelve rank - filter cycles. This problem is the first that needs O(n) storage, so things get rough and I wasn't able to reuse any code from the previous part.
There are many data structures possible, but I go with the trivial one, storing the binary numbers as a 1-D array of raw "1" and "0" characters. Advancing through this buffer with a 12 byte stride gives the corresponding bit (character) from each binary number (string). See `stride-exec`, an iterator-like word that advances by twelves and executes a function at each position.
To filter out a number, overwrite it with blanks, which ensures its digits are no longer "1" or "0". When counting digit frequency, any non"1", non"0" character is sorted into a third category that is ignored (see `strip-sign` and `c-to-idx` for details on how this works).
I do a lazy thing here and store the array on the PAD. Traditionally the PAD is stored a few hundred bytes past the dictionary pointer HERE, so on most Forth systems (including very old ones) it will be more than big enough for a few kilobytes of input text. However, the ANS standard only guarantees [84 characters of space at PAD](https://forth-standard.org/standard/usage#usage:transient) so in some systems this approach might not work, and you might have to manually ALLOT a buffer instead. It works in pforth though.
Because of my choice of data structure, the ranking process is destructive. I need to save a copy of the original data structure before eliminating any values, otherwise the CO2 calculation won't work. After collecting user input with `keys>pad`, I save a second copy of it on the PAD using `pad>buf`. All destructive operations use `buf` and leave the original data alone.
You may notice that I never check that I've narrowed things down to a single number. Instead, I always run twelve passes of filtering, and remember the last value that **met** the bit criteria. This logic is simpler, and although it throws away some best-case performance, its worst-case performance should actually faster than it would be if I'd performed the size check. My aim here was not speed anyway, but to write simpler code.
Finally, at the end of the calculation I have to convert the binary strings back to integers so I can multiply them together and return the result.
# Day 4
# Day 5
Instead of playing with sparse representations, I took the coward's way out with a 1000*1000 array of zero-initialized bytes. This was too big to fit in pforth's dictionary space, so I used [ALLOCATE](https://forth-standard.org/standard/memory/ALLOCATE). This makes me itch a bit because it's way too big to fit in the memory space of classical Forth systems, and I'm sure there's a more efficient way of solving this.
Input parsing is achieved by the old familiar trick of ACCEPT into PAD, but this time there is more work to skip over delimiters and other cruft. I made some words for that: `i>n` to parse a number, and `i>_` to skip past a given delimiter. The magic number 80 is the maximum line length, 44 is the ASCII code for a `,`, and 62 is the ASCII code for the right tip of the `>` arrow separating the two points. Also note that each line of input are now possibly a different length each time (imagine!!!) so before reading each line I clear things out with `pad 81 bl fill`, to make sure there aren't any extra digits lying around getting tacked onto the end of our numbers. All this parsing has no checks at all, and will happily run off the end of memory if you dare provide a malformed input.
Compared to all that mess, drawing lines is easy, and will not be described further here.
For the diagonal lines in Part B, I decided to always draw them from left to right. `normalize-x` is an ugly swap function that swaps around the two points as needed to make sure that drawing from x1,y1 to x2,y2 will always go left-to-right. This narrows things down to two cases: positive slope and negative slope. The function `diag` works out whether a positive or negative slope is needed and then calls `(diag)`, which draws a line of the given slope.
# Day 6
To avoid using tons of memory, I kept a 9-long buffer storing count of lanternfish of each timer value (0..8). I use two buffers so I can page-flip between them at each simulation step.
The pseudocode algorithm is:
```
buffer "old" describes fishes in step 0
new[0 .. 7] = old[1 .. 8]
new[6] += old[0]
new[8] = old[0]
buffer "new" now describes fishes in step 1
```
The same code solved Part A and B of this problem, because my word size is 64 bits and that was enough to track the number of lanternfish after 256 days. For my puzzle input the answer took about 41 bits. Good thing too, because I really didn't want to implement an arbitrary-precision math library.
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment