2026-08-23
Most of this week's work was about adding multi-alias support to the alias Extension for lDebug. Multi-aliases are injected into the debugger command loop using the inject handler rather than only replacing the command in the preprocess handler. This allows multi-aliases to consist of any number of commands, from zero to one to multiple. The old style aliases are called single-aliases.
These changes live in the new ldebug.exp experimental repo for now.
injecting variable, not the current value of the ext_inject_handler variable.)Initially, I believed I would have to keep track of a dedicated "current level" variable. That would seem difficult to manage, especially as an EXIT of a comlevel doesn't imply what level is being exited to. So a stack may have been needed. It turns out that with only the INIT and EXIT callouts as well as the necessary inhibition flags and counters, multi-alias instances can inhibit their injection while more deeply nested comlevels are active.
Technically, a single alias.eld instance has no needs for calling the comlevels handler with alias INIT and EXIT callouts. However, this may become useful eventually, and pathological cases with multiple alias.eld instances are possible to create.
The single alias buffer is now used for two different data types that grow from opposite ends of the buffer towards the middle. This mirrors the design of the lDebug history buffer, as described in a long comment and in some 2021 responses of mine to a 2020 pull request for (FreeDOS) Debug/X.
The advantage is that the shared buffer is used for both types of data, so with less of one type there's more space for the other type of data. The additional setup needed for using a buffer this way is small.
The history buffer is implemented as two arrays that grow towards the middle from either end of the buffer. The first is a byte array storing text data that starts at the low end (start) of the buffer. (This is offset 0 in the separate history segment, if used.) The text is stored back to back, only the content of the lines, no separators or terminators.
The second array gives the *end* of each history entry's text. There are N + 1 array entries, each a 16-bit offset word, where N is the amount of history entries in use. The words are used as displacement from a base that's the start of the history buffer. (Again, the start base is at offset 0 for the separate history segment. However, for simplicity of the code we always do store the actual base start offset into a register and displace from that, even when the actual value of that register will be zero.) The very first entry of the high array is special. It always holds the displacement value zero. When used as a displacement added to the base this points at the very beginning of the entire history buffer (and thus the start of the low array).
The size of a history entry in the low array is obtained by reading both the entry's end displacement from the high array, and the prior entry (at the next higher address) also from the high array to receive the end displacement of the prior history entry. The end displacement of the prior entry is also the start displacement of the current entry. The delta of the two end displacements is the size of this history entry. This also explains why the very first entry of the high array is needed, and why it contains a zero displacement. It is needed in order to determine the start displacement and size of the very first actual history entry. (We could special case the first entry a different way but using the special first entry of the high array that holds a constant zero simplifies the code.)
The history pointers in the word [history.first] as well as word [history.last] point at the very first (special) entry of the high array as well as the very last entry of the high array. (Because the high array grows from the top down, the first entry is at the highest address and the last entry is at the lowest address.) Although the word [history.first] is currently a constant, we use a variable to enable changing the history allocation dynamically if desired later. If the two pointers are one and the same then there is no actual history entry stored in the history. Otherwise, if the distance between the last high array entry and the last low array entry is lower than X + 2 bytes then the history is too full to insert a new entry, where X is the length of the new history entry's text data and the additional 2 bytes are for the high array entry to store the end displacement.
The special first entry of the high array *must* be initialised to hold the displacement value zero. This happens in init.asm for the separate history buffer segment (when initialising the entire segment with zeroes) or in the zeroing of most of the DATASTACK section (when initialising the variables from ..@init_first up to ..@init_behind).
Insertion is simple, if enough space is left in the history buffer: Copy the text to behind the last entry of the low array (this is pointed to by the end displacement in the last high array entry), then create a new last entry of the high array which points behind the text's destination.
Deletion is more difficult. (Generally we will delete the oldest entry, that is the first history entry.) The second entry of the high array needs to be deleted. All subsequent entries of the high array need to move "forward" (towards the higher address), and also must have the length of the text of the entry to delete subtracted. In the low array the text data of the first history entry needs to be overwritten with any subsequent text data; the entire subsequent text data must be moved "forward" (towards the lower address) by the distance that equals the length of the entry's text to delete.
After implementing my own history recall feature in lDebug I compared yours in this PR to mine. Differences:
1. I use 2 bytes for an offset per entry, you are using 4 bytes for the next/previous pointers
2. I don't store the terminating CR as you seem to
3. I don't have to scan a history entry to find its size
4. I don't bother skipping leading whitespace
5. I default to allocate the buffer in its own segment instead of in the debugger's data segment
6. I started with 1 KiB for the data segment buffer and increased the size to 8 KiB for the additional segment buffer, whereas you use about 500 bytes by default
7. I only allow using Up or Down to navigate the history recall if no editing took place yet
8. It appears your history recall is only for rawinput, that is if inDOS is set. This is similar to mine, but I document that and provide ways to enable using rawinput even if the input is from a DOS stdin terminal
9. I allow more than one way to encode the Up/Down arrow keypresses which makes it work on dosemu in -dumb mode
Similarities also include rejecting empty lines and those duplicating the prior line.
First comment on the ticket, 2021-04-20
Correction: It appears your patch does away with the interrupt 21h service 0Ah call entirely and always uses interrupt 16h to read input. In my implementation DOS application terminal input is still done with the DOS service by default, which means the debugger's line history is not available. (I do enter received lines into the history buffer regardless.) And if
rawinputis used (eg DCO flag 800h set) then it can still use interrupt 21h to read input, but using services which read single bytes from stdin.Second comment on the ticket, 2021-04-20