evocation/core.e
Irene Knapp 06b27a0609
typo fix, thanks mei&
Force-Push: yes
Change-Id: I287ce3331bb719b6a8920164f6550b472706b33b
2026-09-21 15:35:41 -07:00

1370 lines
45 KiB
Text

~ ~~~~~~~~~~~~~~~~~~~~~~~~~
~ ~~ Core Forth features ~~
~ ~~~~~~~~~~~~~~~~~~~~~~~~~
~
~ This file provides extremely fundamental functionality which is a
~ necessary component of any Forth dialect, including Evocation. It is
~ included statically as part of any generated executable, and a second copy
~ of it is later copied into the log when that executable runs. Therefore, it
~ is written to obey the constraints of both the label transform, and the
~ log-load transform; see transform.e for more details on that.
~ Execution support
~ ~~~~~~~~~~~~~~~~~
~
~ There's two words, docol and exit, which are essential parts of the
~ indirect-threaded execution model. It would be tempting to put them in
~ execution.e, so they'd be closer to the explanation of what they do, but
~ we need two copies of them (just like we do of every other word in this
~ file), one statically compiled and one in the log. So, they're here, because
~ that's significantly simpler, even though it creates a little bit of extra
~ work for the label transform.
~ Docol is the "interpreter" that is responsible for the semantics of words
~ written as Forth high-level code. The name is said to be short for
~ "do colon", because word definitions begin with a colon.
~
~ Concretely, when interpreting, it saves rsi (the "instruction pointer") to
~ the control stack, takes the address of the codeword from rax and increments
~ it in-place to form the new instruction pointer, and copies that to rsi.
~
~ Having then done this, we're now in the state that normal execution
~ expects, so docol ends by it using "next" to begin the callee's execution,
~ kicking off a nested call.
~
~ At runtime, invoke docol directly and it will return the value that should
~ be used as a codeword. When compiling to a binary executable, the
~ transformation facility needs to reference it directly, and for that
~ purpose, the label "docol-codeword-value" points to the correct place.
~
~ Registers in:
~
~ * rsi is the caller's instruction pointer
~ * rbp is the control stack pointer
~ * rax is the address of the callee's codeword
~
~ Registers out:
~
~ * rsi is the callee's instruction pointer
~ * rbp is the control stack pointer
~ We need this one snippet of assembly code that's just on the log raw,
~ without a word header, because of its role in making words work. We define
~ a label pointing to it.
~
~ This is the only use of labels in core.e. There's no easy way to
~ remove it. We can't use immediate computation based on "here" because
~ the label transform uses the host address space, not the target address
~ space. The transformation facility's support for labels in transformed
~ code was added just for this.
~
~ It may seem as if we could have used some extra space in the middle of
~ the proper docol word, which is defined just below. That would work fine
~ with the label transform, which can do forward references, but the log-load
~ transform's label support is special-cased to ONLY do this, and it will only
~ work with a backward reference.
~
~ We begin by outputting the actual docol routine, the one that codewords
~ should point to. Note that this is before we've done any word header.
~
~ : This is the actual docol routine, the interpreter which is pointed to by
~ : the codeword of every interpreted word. It does not itself have an entry
~ : in the dictionary; the word "docol" will return a pointer to this routine.
~ : The word's entry follows immediately after the routine.
~ : indent
here @
dup L!' docol-codeword-value
:rsi pack-pushcontrol
8 :rax add-reg64-imm8
:rax :rsi mov-reg64-reg64
pack-next
8 packalign
here !
~ : deindent
~ We can't use colon to create docol, not even the part that's a word,
~ because colon tries to dynamically invoke docol to fill in the codeword.
~ : blank-line
~ : This is the dictionary entry for docol.
~ : indent
s" docol" create
self-codeword
here @
L@' docol-codeword-value :rax mov-reg64-imm64
:rax push-reg64
pack-next
8 packalign
here !
~ : deindent
~ Now that we have docol, colon will work and we can define words the normal
~ way.
~ This is the mechanism to "return" from a word interpreted by docol.
~ We pop the control stack, and then, since this is threaded execution, we
~ do the next thing the caller wants to do, by inlining "next".
: exit
[ here @
:rsi pack-popcontrol
here ! ] ;asm
~ Now that we have exit, semicolon will also work and we can define
~ non-assembly words. However, it will be hard to make them do much until we
~ have lit as well. There's no special hurry; most of the words in core.e are
~ assembly words. We'll get to it further down.
~ Stack manipulation routines
~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
~
~ We start with the three traditional stack operations, swap drop and
~ roll. Sorry to fans of the name "ROT"; we were an HP calculator kid. It'll
~ always be roll to us. Anyway, we do a couple other operations too. Since
~ our goal right now is just to bootstrap the heap, we keep this short and
~ sweet.
~
~ This code has been written to satisfy the assumptions needed by the label
~ transform, described in-depth in transform.e. This means that it is, in
~ effect, written in a restricted dialect of Evocation rather than in the full
~ language.
~
~ There is definitely plenty of optimization that could be done.
: swap
[ here @
:rax pop-reg64
:rbx pop-reg64
:rax push-reg64
:rbx push-reg64
here ! ] ;asm
: drop
[ here @
:rax pop-reg64
here ! ] ;asm
: 2drop
[ here @
:rax pop-reg64
:rax pop-reg64
here ! ] ;asm
~ Rotates "up" (pops its parameter, n; nth item then becomes current item).
~
~ We implement this the high-performance way, with rep movsq, aka the
~ instruction that exists to optimize C's memcpy(). The details of setting
~ that up are complex; see below.
: roll
[ here @
~ Pop our parameter. The rep instruction takes rcx as its count, so we
~ reduce copying by using it to hold our count, as well.
:rcx pop-reg64
~ We have n - 1 items to slide, so decrement rcx. For the purpose of
~ counting how many repetitions will happen, it's one-based. This is because
~ the rep instruction performs a single movsq, then decrements rcx, then
~ stops if rcx is zero.
:rcx dec-reg64
~ Retrieve the nth item, for later. For this purpose we're thinking in
~ zero-based terms, so we do this after already having decremented rcx.
:rsp :rcx 8 :rbx mov-reg64-indexed-reg64
~ The source address for movsq is rsi and the destination is rdi; we can
~ use rdi as we wish, but rsi is our Forth "instruction pointer", so we must
~ save and restore it. Doing so alters rsp, so we have to adjust the address
~ calculations by eight bytes as compared to the expressions above, but
~ happily we can use the disp8 field to do that. We'd be using disp8 anyway
~ because it's helpful.
:rsi push-reg64
~ Now we set up parameters for the memory-sliding operation. We have
~ n - 1 items to copy, moving the range rsp through rsp + (n-2)*8 onto the
~ range rsp + 8 through rsp + (n-1)*8. That's with the value of rsp as it
~ exists at this moment (it's going to change soon).
~
~ We're sliding them upwards in memory, so we start at the high end so
~ that we're always moving into a location that doesn't have anything
~ precious. We use lea as a convenient way to do the stack math.
~
~ When rcx is 1, we want rsp + 8.
:rsp :rcx 8 :rsi lea-reg64-indexed-reg64
~ When rcx is 1, we want rsp + 16.
:rsp :rcx 8 8 :rdi lea-reg64-disp8-indexed-reg64
~
~ Using rcx = 1 is the most convenient example to use for figuring out the
~ arithmetic. It's a linear relationship, so as long as we get the 8-byte
~ stride correct, we just need to pick a single point and verify that our
~ math is right for that point, and it'll be right for any value of rcx.
~ Another of our Forth conventions is that the DF flag should be kept at
~ zero, which directs string instruction to increment rsi. Here, however,
~ because our source and destination ranges overlap, we need to start at the
~ high end, which means we need it to decrement. So we set DF to one, and
~ we'll clear it after.
std
rep-movs64
~ Set everything back.
cld
:rsi pop-reg64
~ There is now an extra item at the low end of the stack (the top) that
~ needs to go away, and coincidentally we have a value in rbx that needs to
~ be in that spot. Rather than doing a drop and push, we overwrite it, to
~ save a little work.
:rbx :rsp mov-indirect-reg64-reg64
~ All done, wow! What a mouthful.
here ! ] ;asm
~ Rotates "down" (pops its parameter, n; current item then becomes nth
~ item).
~
~ Jonesforth calls this "-roll" and we could do that, but honestly the name
~ unroll sounds nicer and it's only a single character longer. You might say
~ it rolls off the tongue better.
~
~ We implement this the high-performance way, with rep movsq, aka the
~ instruction that exists to optimize C's memcpy(). The details of setting
~ that up are complex; see below.
: unroll
[ here @
~ Pop our parameter. The rep instruction takes rcx as its count, so we
~ reduce copying by using it to hold our count, as well.
:rcx pop-reg64
~ We have n - 1 items to slide, so decrement rcx. Also, save a copy of it in
~ rdx after doing that, for later.
:rcx dec-reg64
:rcx :rdx mov-reg64-reg64
~ Retrieve the 0th item, for later.
:rsp :rbx mov-reg64-indirect-reg64
~ Now we set up parameters for the memory-sliding operation. We have
~ n - 1 items to copy, moving the range rsp + 8 through rsp + (n-1)*8 onto
~ the range rsp through rsp + (n-2)*8. That's with the value of rsp as it
~ exists at this moment (it's going to change soon).
~
~ We're sliding them downwards in memory, so we start at the low end so
~ that we're always moving into a location that doesn't have anything
~ precious. We use lea as a convenient way to do the stack math.
~
~ As with roll, we need to save rsi and adjust those rsp calculations
~ accordingly.
:rsi push-reg64
~ Regardless of rcx, we want rsp + 16.
:rsp 16 :rsi lea-reg64-disp8-reg64
~ Regardless of rcx, we want rsp + 8.
:rsp 8 :rdi lea-reg64-disp8-reg64
~ With roll, we were starting at the high end. Here, we start at the low
~ end, which means we need rsi to increment after each repetition. That's
~ what it does when the DF flag is clear, and another of our Forth
~ conventions is to keep it clear normally. So, we don't have to touch DF!
~ Yay!
rep-movs64
~ Restore our original rsi.
:rsi pop-reg64
~ There is now an extra item in the middle of the stack, at the high end of
~ the sliding we did, that needs to be overwritten with our value in rbx.
~ Since we destructively updated our count in rcx, we saved a copy of the
~ count in rdx, and we use that to find the right address.
~
~ When the original count was n, we want rsp + (n-1)*8, so we saved rdx
~ after decrementing rcx, above.
:rbx :rsp :rdx 8 mov-indexed-reg64-reg64
~ All done, wow! What a mouthful.
here ! ] ;asm
~ Rotates "up" (third item becomes current item)
: 3roll
[ here @
:rax pop-reg64
:rbx pop-reg64
:rcx pop-reg64
:rbx push-reg64
:rax push-reg64
:rcx push-reg64
here ! ] ;asm
~ Rotates "down" (current item becomes third item)
: 3unroll
[ here @
:rax pop-reg64
:rbx pop-reg64
:rcx pop-reg64
:rax push-reg64
:rcx push-reg64
:rbx push-reg64
here ! ] ;asm
: dup
[ here @
:rax pop-reg64
:rax push-reg64
:rax push-reg64
here ! ] ;asm
: 2dup
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx push-reg64
:rax push-reg64
:rbx push-reg64
:rax push-reg64
here ! ] ;asm
~ Arithmetic routines
~ ~~~~~~~~~~~~~~~~~~~
~
~ No surprises here. Again, since our goal is to bootstrap the heap, we
~ keep it short. Also again, this is nowhere near optimal.
: +
[ here @
:rbx pop-reg64
:rax pop-reg64
:rbx :rax add-reg64-reg64
:rax push-reg64
here ! ] ;asm
: -
[ here @
:rbx pop-reg64
:rax pop-reg64
:rbx :rax sub-reg64-reg64
:rax push-reg64
here ! ] ;asm
: *
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx mul-reg64
:rax push-reg64
here ! ] ;asm
~ Jonesforth calls this "/mod" but % is widely recognized and has no special
~ Forth significance.
: /%
[ here @
:rdx :rdx xor-reg64-reg64 ~ rdx is the high bits of the input; zero it
:rbx pop-reg64
:rax pop-reg64
:rbx divmod-reg64
:rdx push-reg64 ~ remainder
:rax push-reg64 ~ quotient
here ! ] ;asm
~ We define a pair of words that increment and decrement a single item.
~ These were inspired by Common Lisp, where they're constantly useful.
: 1+
[ here @
:rax pop-reg64
:rax inc-reg64
:rax push-reg64
here ! ] ;asm
: 1-
[ here @
:rax pop-reg64
:rax dec-reg64
:rax push-reg64
here ! ] ;asm
~ Comparison routines
~ ~~~~~~~~~~~~~~~~~~~
~
~ So. This is subtle. These comparison routines all have the same structure.
~ Notice that = and != are commutative, whereas the others are not, so
~ consider > as the archetypical one when reasoning about correctness. To
~ test if A > B, we do cmp A, B, which sets the flags the same way as
~ subtracting B from A does. The mnemonic names of the condition codes are
~ based on the assumption you do it in this order.
~
~ We want to treat the top of the stack as the first operand, so we
~ carefully pop in the appropriate order.
~
~ There are both signed and unsigned variants of the condition codes. We
~ provide both; the unmarked comparisons such as > and <= are signed, while
~ the marked ones such as >unsigned and <=unsigned are of course unsigned.
~ Jonesforth calls this "="; most languages would call it "==".
: =
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-equal :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Jonesforth calls this "<>", but even most modern SQL dialects recognize
~ C's legacy and allow "!=" these days. As someone who learned C in childhood,
~ this is not actually a hard call for us.
: !=
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-not-equal :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Is the top of the stack greater than the second item in the stack, when
~ both are treated as signed?
: >
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-greater :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Is the top of the stack less than the second item in the stack, when both
~ are treated as signed?
: <
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-less :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Is the top of the stack greater than or equal to the second item in the
~ stack, when both are treated as signed?
: >=
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-greater-equal :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Is the top of the stack less than or equal to the second item in the
~ stack, when both are treated as signed?
: <=
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-less-equal :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Is the top of the stack greater than the second item in the stack, when
~ both are treated as unsigned?
: >unsigned
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-above :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Is the top of the stack less than the second item in the stack, when
~ both are treated as unsigned?
: <unsigned
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-below :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Is the top of the stack greater than or equal to the second item in the
~ stack, when both are treated as unsigned?
: >=unsigned
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-above-equal :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Is the top of the stack less than or equal to the second item in the
~ stack, when both are treated as unsigned?
: <=unsigned
[ here @
:rax pop-reg64
:rbx pop-reg64
:rbx :rax cmp-reg64-reg64
:cc-below-equal :al set-reg8-cc
0x01 :rax and-reg64-imm8
:rax push-reg64
here ! ] ;asm
~ Bitwise routines
~ ~~~~~~~~~~~~~~~~
~
: &
[ here @
:rbx pop-reg64
:rax pop-reg64
:rbx :rax and-reg64-reg64
:rax push-reg64
here ! ] ;asm
: |
[ here @
:rbx pop-reg64
:rax pop-reg64
:rbx :rax or-reg64-reg64
:rax push-reg64
here ! ] ;asm
: xor
[ here @
:rbx pop-reg64
:rax pop-reg64
:rbx :rax xor-reg64-reg64
:rax push-reg64
here ! ] ;asm
~ The HP overloads the name "not", so we follow the Forth convention.
: invert
[ here @
:rax pop-reg64
:rax not-reg64
:rax push-reg64
here ! ] ;asm
~ Literal routines
~ ~~~~~~~~~~~~~~~~
~
~ These words are rarely used directly from user code, but are emitted by
~ the interpreter, in compile mode. They allow literal values of various kinds
~ to exist inline as part of compiled code, which would otherwise be only an
~ array of codeword pointers. They do this, in their various ways, by pushing
~ any appropriate value on the stack and making sure execution skips over the
~ bytes used to represent it.
: lit
[ here @
lods64
:rax push-reg64
here ! ] ;asm
: litstring
[ here @
~ The string immediately follows the codeword in memory, so rsi is
~ already pointing to it. That address will be our returned result, so we
~ push it to the stack.
:rsi push-reg64
~ Now we need to skip over the string, so that rsi will be valid for the
~ next Forth word. To do that, we're going to do a string operation with
~ scas8, which takes rdi as the address to look at. This means scas8 is
~ treating its operand as analogous to the destination operand of movs*.
:rsi :rdi mov-reg64-reg64
~ We want to compare for equality with zero; scas8 looks in al for the
~ other half of the comparison, so we clear rax.
:rax :rax xor-reg64-reg64
~ Counterintuitively, we do need to pass a count. We pass -1, which will
~ always work.
:rcx :rcx xor-reg64-reg64
:rcx not-reg64
~ The DF flag is zero per our Forth execution-model convention, which
~ means scas8 increments rdi after each iteration. This is what we want,
~ so there's no need to mess with it.
~
~ We also need to worry about ZF, but fortunately that "not" will make
~ sure it's clear.
~
~ So, we're ready; do it!
repnz-scas8
~ The scas8 completes, incrementing rdi, before the repnz checks its
~ condition. So, rdi is now pointing immediately after the terminating
~ null byte. Of course, we want this in rsi for Forth's purposes.
:rdi :rsi mov-reg64-reg64
~ Finally, we need to align rsi to the next word boundary.
7 :rsi add-reg64-imm8
7 invert :rsi and-reg64-imm8
here ! ] ;asm
~ Memory access routines
~ ~~~~~~~~~~~~~~~~~~~~~~
~
~ We go with Forth names for this stuff. The HP's names for memory and
~ storage operations heavily leverage the fact they have an object system
~ with type tags and so on; we want to stay close to the bytes.
~ (value, address --)
: !
[ here @
:rbx pop-reg64
:rax pop-reg64
:rax :rbx mov-indirect-reg64-reg64
here ! ] ;asm
~ (address -- value)
: @
[ here @
:rax pop-reg64
:rax :rax mov-reg64-indirect-reg64
:rax push-reg64
here ! ] ;asm
~ This follows the general convention that words that set things take the
~ destination as the final parameter.
~
~ (value, address --)
: +!
[ here @
:rbx pop-reg64
:rax pop-reg64
:rax :rbx add-indirect-reg64-reg64
here ! ] ;asm
~ (value, address --)
: -!
[ here @
:rbx pop-reg64
:rax pop-reg64
:rax :rbx sub-indirect-reg64-reg64
here ! ] ;asm
~ Jonesforth calls this "c!" but we categorically reject the use of letters
~ that are meant to indicate byte sizes. It's unfriendly to newcomers. There's
~ some risk that this name will be confused for meaning "store a value of 8",
~ but that would not be a useful task, so hopefully it'll be okay.
: 8!
[ here @
:rbx pop-reg64
:rax pop-reg64
:al :rbx mov-indirect-reg64-reg8
here ! ] ;asm
: 8@
[ here @
:rbx pop-reg64
:rax :rax xor-reg64-reg64
:rbx :al mov-reg8-indirect-reg64
:rax push-reg64
here ! ] ;asm
: 16!
[ here @
:rbx pop-reg64
:rax pop-reg64
:ax :rbx mov-indirect-reg64-reg16
here ! ] ;asm
: 16@
[ here @
:rbx pop-reg64
:rax :rax xor-reg64-reg64
:rbx :ax mov-reg16-indirect-reg64
:rax push-reg64
here ! ] ;asm
: 32!
[ here @
:rbx pop-reg64
:rax pop-reg64
:eax :rbx mov-indirect-reg64-reg32
here ! ] ;asm
: 32@
[ here @
:rbx pop-reg64
:rax :rax xor-reg64-reg64
:rbx :eax mov-reg32-indirect-reg64
:rax push-reg64
here ! ] ;asm
~ Before we get too deep into it, we also define a few reflection routines
~ that retrieve, or set, the address of either of the two stacks.
~
~ The result, or the new value, is on the top of the data stack. That's the
~ same as how it works for any other word, but given the metacircular nature
~ of these ones it's easy to get confused about that...
~
~ Jonesforth calls this RSP!, which looks as if it's meant to be an Intel
~ register name but is actually short for return stack pointer. There is no
~ register by that name, it's a Forth-provided abstraction. That's super
~ confusing, plus as discussed above we call it the control stack not the
~ return stack, so we call the words...
~
~ (stack pointer --)
: control!
[ here @
:rbp pop-reg64
here ! ] ;asm
~ (-- stack pointer)
: control@
[ here @
:rbp push-reg64
here ! ] ;asm
~ Jonesforth calls this DSP!, for data stack pointer. Again, there's no
~ Intel register by that name, and we call it the value stack, so...
~
~ (stack pointer --)
: value!
[ here @
~ Per Intel's description of POP this reads from the old location, and
~ there is no increment applied to the resulting value. See, the
~ description says it increments the register then overwrites it, in that
~ order.
:rsp pop-reg64
here ! ] ;asm
~ (-- stack pointer)
: value@
[ here @
~ Per Intel's description of PUSH this pushes the old value.
:rsp push-reg64
here ! ] ;asm
~ Copy one non-overlapping block of memory over another. For the overlapping
~ logic, see roll and unroll. This always copies at byte granularity, for ease
~ of implementation.
~
~ Jonesforth calls this CMOVE and has a CCOPY which is a single byte. POSIX,
~ however, has memcpy() for non-overlapping blocks and memmove() for
~ overlapping blocks. We follow the latter convention, because it feels like
~ the more important distinction.
~
~ Also, the "c" was meant to indicate that it works at one-byte granularity,
~ but that isn't, uh... actually an important property here, and as a blanket
~ call we're not using letters to denote data sizes. So we call it "memcopy".
~ Apologies to the C programming tradition but vowels are good, actually.
~
~ Jonesforth also offers C@C! as another name for its CCOPY, but neither
~ "@!" nor "mem@mem!" seems particularly nice.
~
~ (source, destination, length --)
: memcopy
[ here @
~ We need to save and restore rsi; the other registers we can trample.
:rsi :rdx mov-reg64-reg64
:rcx pop-reg64
:rdi pop-reg64
:rsi pop-reg64
~ We start from the low end, since that's easier arithmetic. So, we get
~ to leave the DF flag alone.
rep-movs8
:rdx :rsi mov-reg64-reg64
here ! ] ;asm
~ This does the same thing as memcopy, but correctly handles situations
~ where the destination overlaps with the source. It achieves this by being
~ careful about which end the transfer starts from. This "move" vs. "copy"
~ distinction mirrors C terminology.
~
~ (source, destination, length --)
: memmove
[ here @
~ We need to save and restore rsi; the other registers we can trample.
:rsi :rdx mov-reg64-reg64
:rcx pop-reg64
:rdi pop-reg64
:rsi pop-reg64
~ We need to check source < destination to decide which end to start from.
:rsi :rax mov-reg64-reg64
:rdi :rax cmp-reg64-reg64
~ Relative offsets are from the start of the instruction after the jmp.
4 :cc-below jmp-cc-rel-imm8
~ If source is greater, we are sliding downwards so we start from the low
~ end. So, we get to leave the DF flag alone.
rep-movs8 ~ 2 bytes
16 jmp-rel-imm8 ~ 2 bytes
~ If destination is greater, we are sliding upwards so we start from the
~ high end. So, we have to save and restore DF. Also, we have to adjust the
~ pointers.
:rcx :rsi add-reg64-reg64 ~ 3 bytes
:rsi dec-reg64 ~ 3 bytes
:rcx :rdi add-reg64-reg64 ~ 3 bytes
:rdi dec-reg64 ~ 3 bytes
std ~ 1 byte
rep-movs8 ~ 2 bytes
cld ~ 1 byte
:rdx :rsi mov-reg64-reg64
here ! ] ;asm
~ (string pointer -- string length not including null byte)
: stringlen
[ here @
:rdi pop-reg64
:rdi :rbx mov-reg64-reg64
:rax :rax xor-reg64-reg64
:rcx :rcx xor-reg64-reg64
:rcx not-reg64
repnz-scas8
:rbx :rdi sub-reg64-reg64
1 :rdi sub-reg64-imm8
:rdi push-reg64
here ! ] ;asm
~ This word is for working with strings that are null-terminated at both
~ ends. It counts backwards from the end to find the string length. It accepts
~ an input pointer to the final non-null byte at the end of the string, and
~ returns the length not including either null byte.
~
~ (string end pointer -- string length not including null)
: reverse-stringlen
[ here @
:rdi pop-reg64
:rdi :rbx mov-reg64-reg64
:rax :rax xor-reg64-reg64
:rcx :rcx xor-reg64-reg64
:rcx not-reg64
std
repnz-scas8
cld
:rdi :rbx sub-reg64-reg64
1 :rbx sub-reg64-imm8
:rbx push-reg64
here ! ] ;asm
~ If you have a variable-length string followed by alignment padding, and
~ you want to traverse it in reverse, you also need to skip the alignment
~ padding...
~
~ The alignment padding can have a total length of no more than 8 bytes. The
~ returned value counts from the end, backwards, to the nearest non-null byte,
~ or at most a distance of eight.
~
~ (address of final null byte in padding -- count of null bytes in padding)
: reverse-padding-len
[ here @
:rdi pop-reg64
:rdi :rbx mov-reg64-reg64
:rax :rax xor-reg64-reg64
9 :rcx mov-reg64-imm32
std
repz-scas8
cld
:rdi :rbx sub-reg64-reg64
1 :rbx sub-reg64-imm8
:rbx push-reg64
here ! ] ;asm
~ We make this work using exactly two jump instructions, which is likely the
~ minimum possible. To avoid relying on labels, we hand-compute the byte
~ offsets, so every instruction within their ranges is annotated with its
~ length in bytes. Good fun.
~
~ The returned comparsion value is 0 for equal, 1 to indicate the left
~ string (lower on the stack) is greater, and -1 to indicate the right string
~ (top of the stack) is greater.
~
~ (left string pointer, right string pointer -- comparison value)
: stringcmp
[ here @
~ Save the rsi register.
~
~ Happily, we don't need a lot of registers for this code, so we can
~ dedicate rdx to this and not have to deal with stack juggling..
:rsi :rdx mov-reg64-reg64
~ Get the parameters off the stack.
~
~ For reasons that will be explained below, the left and right strings
~ from our caller's perspective are swapped from the perspective of the
~ comparisons we'll be doing. The rdi register points to the right-hand
~ operand of the "cmps" instruction, while rsi points to the left-hand
~ operand; [Intel] volume 2A, chapter 3, section 4-3.3, "CMPS"; note that
~ the PDF mis-numbers the sections in the second half of chapter 3.
:rsi pop-reg64
:rdi pop-reg64
~ For the comparison-unequal loop-exit code, we'll need rbx set to zero.
~ The easy way to do that is by xor'ing it with itself, but doing it at
~ the end of the loop would overwrite the comparison flags, which we need.
~ So we do it in advance, and carefully don't touch it anywhere else.
~
~ See the list of which instructions affect the flags in [Intel]
~ volume 1, appendix A, section A-1, table A-2.
:rbx :rbx xor-reg64-reg64
~ We also clear rax; we'll be using al later, and it will be convenient
~ to know the upper bits are always zero.
:rax :rax xor-reg64-reg64
~ Now we've initialized everything we need; everything after this point is
~ part of the loop.
~ At the start of each iteration, save a copy of the byte we're at now.
~ We need to do this before cmps because the pointers will increment;
~ we'll eventually use it to test whether we've reached the end delimiter.
~
~ We do a 64-bit load into rcx, which is otherwise unused, then copy the
~ low byte from cl to al. This avoids having to think about addressing
~ modes that combine 64-bit addresses with smaller data sizes; those are
~ very subtle.
:rsi :rcx mov-reg64-indirect-reg64 ~ 3 bytes
:cl :al mov-reg8-reg8 ~ 2 bytes
~ Now do the cmps, which is the heart of the loop.
~
~ Note that, in addition to relying on this comparison to test content
~ bytes against each other, in the event that one string is a prefix of
~ the other, this will also test content bytes against null delimiters.
~ The longer string will compare as "greater", because zero is less than
~ all other possible byte values. This is what we want.
~
~ Since strings of different lengths are necessarily unequal, letting
~ this test do the work of detecting that also means we don't have to deal
~ with scenarios where we're past the end of one string but not the other.
cmps8 ~ 1 byte
~ The flags are now set based on the simulated subtraction of the next
~ bytes from the two strings. If they were unequal, the loop will end. If
~ they were equal, we have another test to do before we're finished with
~ this iteration. So we put the loop-end code next, and conditionally jump
~ forward past it.
~
~ Recall that relative offsets are from the start of the instruction after
~ the jmp.
15 :cc-equal jmp-cc-rel-imm8 ~ 2 bytes
~ If we got here, the strings are unequal, so we need to turn the flags
~ into a comparison value. We cleared rbx earlier; now we set its low bit
~ to the "above" flag, then use sbb to subtract the "carry" flag (which it
~ thinks of as the "borrow" flag).
~
~ Recall that "carry" is based on whether the comparison would cause a
~ change to the next bit "outside" the bits being compared; [Intel]
~ volume 1, chapter 3, section 3-4.3.1. Since the comparison is a
~ subtraction, "carry" is true when the right-hand byte is strictly
~ greater than the left-hand byte.
~
~ Also recall that "zero", also called "equal", is based on whether the
~ comparison's result produces output that's all zero bits. Since the
~ comparison is a subtraction, "zero" is true when the two bytes are
~ identical.
~
~ The "above" condition is not a flag, but a composite of flags. It's
~ true when both "carry" and "zero" are false; [Intel] volume 1, chapter
~ 7, section 7-3.1.1, table 7-2. For us, this is equivalent to saying the
~ left-hand byte is strictly greater than the right-hand.
~
~ So, after the "set" instruction, rbx is 1 if left > right, and 0 if
~ left = right or left < right. The "sbb" instruction with an immediate
~ value of zero subtracts the value of the carry flag from rbx; the flag
~ is true in the case where left < right, and false otherwise. So, after
~ the "sbb", rbx is 1 if left > right, 0 if left = right, and -1 if
~ left < right.
~
~ This sounds like the opposite of what we want, but recall that we
~ exchanged the operands, so these are the values our caller is expecting.
~ While it might be tempting to look for a way to not need that
~ transposition, and indeed there exists an approach using "not above",
~ it's fiddly in a way that's even harder to explain.
~
~ Note that it's important that the "jmp" and "set" instructions don't
~ change the flags (see table A-2 again), so the "cmps" is the most recent
~ thing that did. The "sbb" instruction does change them, but we don't
~ need them again after that point so it doesn't matter.
~
~ Finally, we need to restore rsi before we return to Forth.
~
~ The two-instruction set-sbb sequence is one of those classic assembly
~ programming tricks that often goes unexplained, because most of its
~ appeal is its brevity and a proper explanation is quite lengthy. Please
~ enjoy this gift of knowledge, and thanks to our friends who showed it to
~ us.
:cc-above :bl set-reg8-cc ~ 3 bytes
0 :rbx sbb-reg64-imm8 ~ 4 bytes
:rbx push-reg64 ~ 1 byte
:rdx :rsi mov-reg64-reg64 ~ 3 bytes
pack-next ~ 4 bytes
~ If we got here, the current bytes are equal to each other. We still
~ need to test if they're null terminators; if so, we exit, and if not, we
~ loop.
~
~ At the beginning of this iteration, we saved a copy of the byte being
~ inspected in rax. The "test" instruction simulates a xor and sets the
~ flags accordingly. We check the "not equal" condition, also known as
~ "not zero", and jump backwards to the start of the loop when that's the
~ case.
~
~ Recall that relative offsets are from the start of the instruction
~ after the jmp.
:rax :rax test-reg64-reg64 ~ 3 bytes
-28 :cc-not-equal jmp-cc-rel-imm8 ~ 2 bytes
~ If we got here, we got all the way to the end of both strings and
~ found a null byte, so we return 0 to indicate they're equal.
~
~ We can use push-imm32-extended64 even though our stack holds 64-bit
~ values, because it gets sign-extended.
~
~ We need to restore rsi before we return to Forth.
0 push-imm32-extended64
:rdx :rsi mov-reg64-reg64
here ! ] ;asm
~ Branching
~ ~~~~~~~~~
~ Okay, so there's a weird thing about how branch and 0branch are defined.
~ 0branch jumps into branch, so they need to be next to each other in the log.
~ To compute the offset to jump by, we pass an address on the stack. Notice
~ that assembly words are always defined in immediate mode; it's just they
~ usually only do trivial logic.
~ This takes a number of bytes, not machine words. That allows it to be used
~ for putting weird things embedded in the code.
~
~ The offset is relative to the start of the word the number of bytes is in,
~ so, make sure to have it skip itself.
: branch
[ here @
dup swap ~ Save a copy of this address for later.
:rsi :rsi add-reg64-indirect-reg64
here ! ] ;asm
~ (jump destination)
: 0branch
[ here @
~ (jump destination, output point)
:rax pop-reg64
:rax :rax test-reg64-reg64
~ Please notice the 8-bit branch to the nearby word. This is the offset
~ from the end of the jmp instruction, to the start of the target
~ instruction. To avoid needing the label system for this, we compute it
~ based on the address before the jmp; the jmp itself is two bytes.
~
~ Remember, with jump arithmetic, subtract the start from the
~ destination.
~
~ What we're testing with :cc-equal is that the input value is zero.
~ The name is slightly counterintuitive; it's a result of the condition
~ names favoring cmp over test. While cmp simulates subtraction, test
~ simulates bitwise AND.
dup 2 + 3roll swap - :cc-equal jmp-cc-rel-imm8
~ (output point)
~ In the event we didn't jump, we still need to skip over the literal
~ value. Using lods here is just a convenient way to skip rsi forward.
lods64
here ! ] ;asm
~ the stack is empty once more
~ This is like next, but instead of using rsi as the "instruction pointer",
~ it takes a codeword address from the value stack.
~
~ In the event that the codeword is docol, docol will handle any
~ manipulation of the control stack that needs to happen. Yes, it really is
~ that simple.
~
~ (execution token --)
: execute
[ here @
:rax pop-reg64
:rax jmp-abs-indirect-reg64
here ! ] ;asm
~ Dictionary entries
~ ~~~~~~~~~~~~~~~~~~
~
~ Now, we have a bunch of words that are used for traversing the Forth
~ core data structures that describe words. First, we have a couple that
~ relate to individual words and their pieces...
~
~ The log-load transform produces code that requires
~ entry-to-execution-token, which means it's needed statically. So this stuff
~ to deal with word entry headers might as well go in core, since it has no
~ dependencies to speak of.
~
~ These are the first words in core that are implemented in Forth rather
~ than assembler. That's not as big a deal as it may seem; the Forth execution
~ model has been ready-to-go ever since we implemented docol and exit, and
~ at this point we have enough basics to do useful things with it.
~ Jonesforth calls this "TFCA" and ">CFA"; its author speculates that the
~ original meaning is "code field address".
~
~ (entry pointer -- execution token)
: entry-to-execution-token
~ Skip next-entry pointer, flag byte, and start terminator.
10 +
~ Skip string contents.
dup stringlen +
~ Skip one for the null terminator, seven more for alignment.
8 +
~ Zero the low bits and now it's aligned.
7 invert & ;
~ Jonesforth calls this "CFA>". Jonesforth's implementation searches the
~ entire dictionary, since its word header format isn't designed to be
~ traversed in reverse, but ours is, so it should be fast.
~
~ (execution token -- entry pointer)
: execution-token-to-entry
1 -
dup reverse-padding-len -
dup reverse-stringlen -
9 - ;
~ (entry pointer -- flags byte)
: entry-flags@
8 + @ 0xFF & ;
~ (new flags byte, entry pointer --)
: entry-flags!
8 +
dup @ 3roll
0xFF &
swap 0xFFFFFFFFFFFFFF00 & |
swap !
;
~ (entry pointer -- name string pointer)
: entry-to-name 10 + ;
~ Binary packing
~ ~~~~~~~~~~~~~~
~
~ These routines are for building up data structures in-memory. Sometimes
~ they're used for structures that are meant to stay in memory; other times
~ it's a buffer that will become output.
~
~ The general pattern is that each routine takes an output address and
~ some specific datum, and returns the output address adjusted to point
~ after the new datum. That makes them easy to chain together. We call this
~ address the "output point", to capture the idea that it's a running total
~ which gets updated by each new datum as it's packed.
~ (output point, value -- output point)
: pack64 swap dup 3unroll ! 8 + ;
: pack32 swap dup 3unroll 32! 4 + ;
: pack16 swap dup 3unroll 16! 2 + ;
: pack8 swap dup 3unroll 8! 1 + ;
~ This works on C-style strings, which are characters followed by a null
~ terminator. The packed data includes the null terminator.
~
~ (output point, string pointer -- output point)
: packstring
dup stringlen 1 + dup
~ (output point, source, length, length)
4 roll dup 4 unroll
~ (source, destination, length, length, output point)
+ 4 unroll
~ (output point, source, destination, length)
memcopy ;
~ This also works on C-style strings, relying on the null terminator, but
~ the packed data does not include it.
~
~ (output point, string pointer -- output point)
: pack-raw-string
dup stringlen dup
~ (output point, source, length, length)
4 roll dup 4 unroll
~ (source, destination, length, length, output point)
+ 4 unroll
~ (output point, source, destination, length)
memcopy ;
~ (output point, alignment byte width -- output point)
: packalign
~ It's possible the padding will be of length zero; keep that in mind
~ while maintaining these magic comments, as it comes up in several ways.
~ : fresh-line
~ : 0 #-- (alignment padding)
{ 2dup /% drop { drop exit } unless
swap 0 pack8 swap
~ : 1 adjust-length
} forever ;
~ Binary unpacking
~ ~~~~~~~~~~~~~~~~
~
~ These routines are for examining data structures in-memory.
~
~ Similarly to the output routines, each routine takes an input address,
~ which it updates to point after the data item being read. We call this the
~ "input point". Since this is input, the routines return data items rather
~ than accepting them.
~ (input point -- input point, value)
: unpack64 dup @ swap 8 + swap ;
: unpack32 dup 32@ swap 4 + swap ;
: unpack16 dup 16@ swap 2 + swap ;
: unpack8 dup 8@ swap 1 + swap ;
~ (proposed size, alignment byte width -- adjusted size)
: align-size
dup 3unroll dup 3unroll
~ (alignment, alignment, proposed size, alignment)
1 - + swap /% swap drop * ;
~ You might think this would be identical to packalign, but packalign has
~ side effects.
~
~ (input point, alignment byte width -- input point)
: unpackalign align-size ;
~ The first hint of a debugging tool
~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ This peforms the "hlt" instruction (Intel's mnemomic, short for "halt"),
~ which will cause the program to exit with a segmentation fault. If you're
~ running under a debugger, this is a convenient way to get execution to stop
~ at a certain point.
~
~ It's called "crash" rather than "hlt" to distinguish it from the word
~ which outputs the instruction as machine code.
~
~ This is the main diagnostic tool available under the label transform. Yep.
~ That's all we get. Don't make those eyes.
: crash
[ here @
hlt
here ! ] ;asm
~ Additional conveniences
~ ~~~~~~~~~~~~~~~~~~~~~~~
~
~ Please feel free to find an organizing principle for these and put them
~ where they go. It would be kind of nice to re-write these either in
~ assembly, or with high-level flow control, but it doesn't seem urgent...
: max
2dup >= { swap } if
drop ;
: min
2dup <= { swap } if
drop ;
: over swap dup 3unroll ;
~ 0 pick is equivalent to dup. 1 pick is equivlent to over.
: pick 2 + dup roll dup 3roll unroll ;
~ Standard Forth doesn't have equivalents of our ndrop and ndup. The HP
~ calls them DROPN and DUPN but that doesn't go well with ie. 2dup or 3roll,
~ so we do it like this.
: ndrop { dup } { swap drop 1- } while drop ;
: ndup
dup 1+ swap
{ dup } { swap dup pick 3unroll swap 1- } while
2drop ;
: 3drop drop drop drop ;
: 3dup 2 pick 2 pick 2 pick ;
: && 0 != swap 0 != * ;
: || | 0 != ;
: not 0 = ;
: negate -1 * ;
: align-floor dup 3unroll /% swap drop * ;
~ Find-in is the main word that provides the capability to look up words by
~ name, though it's usually used via "find" rather than being called directly.
~
~ Find-in traverses the linked list formed by a particular dictionary's
~ next-entry pointers, looking for an entry that matches a given name. The
~ dictionary pointer is the pointer (not handle) to the root of the list,
~ which runs from newest to oldest. For example, dereferencing the value of
~ "latest" gives the pointer to the main dictionary, which can be passed to
~ find-in.
~
~ Having find-in separated out is convenient when working with alternate
~ dictionaries, but the main reason for having it is not convenience but
~ necessity: During Evocation's startup, there is a period before global
~ variables are easily accessible, so there would be no way to implement
~ "find". The warm-start routine (see execution.e and transform.e) has the
~ job of fixing that, and it makes extensive use of find-in to do so.
~
~ The definition of "find" is in dynamic.e, with the other stuff that needs
~ to access the log.
~
~ (dictionary pointer, string pointer -- entry pointer or 0)
: find-in
~ It will be more convenient to have the entry pointer on top.
swap
{
~ If the entry pointer is null, exit.
~ (name pointer to find, current entry pointer)
dup 0 = { swap drop exit } if
~ Check this entry's "hidden" flag.
~ (name pointer to find, current entry pointer)
dup entry-flags@ 0x80 & 0x80 != {
~ Test whether this entry is a match.
~ (name pointer to find, current entry pointer)
2dup 10 + stringcmp 0 = {
~ If we're here, it's a match. Clean up our working state and exit.
~ (name pointer to find, current entry pointer)
swap drop exit
} if
} if
~ If we're here, it's not a match; traverse the pointer and repeat.
~ (name pointer to find, current entry pointer)
@
} forever ;