User Tools

Site Tools


blog:pushbx:2026:0823_mid_late_august_work_multi-aliases

Mid late August work: Multi-aliases

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.

LBACACHE

lDebug

These changes live in the new ldebug.exp experimental repo for now.

  • alias.asm:
    • Extract function hookinject, which makes sure that the inject handler is hooked. (It determines that it is hooked using the ELD's own injecting variable, not the current value of the ext_inject_handler variable.)
    • Comment on an error condition of corrupted AC if the alias command type exceeds ACT_MAX.
    • Implement a simple inhibition mechanism for comlevels precedence.
    • Clamp the counter decrement for inhibition to zero, so unexpected EXIT calls won't underflow.
    • Upon deletion (of a non-busy alias), relocate the active instances' pointers to their busy aliases.
    • Add the debugger's call_comlevel_bx_ax function which is for use by alias.eld
    • Implement alias.eld calling out to the comlevels handlers to indicate INIT and EXIT of alias command levels. This is required to correctly support multiple alias.eld instances installed residently at the same time. It was tricky to juggle the various hook and unhook opportunities of the inject handler, which I completely failed to account for initially during the creation of this changeset. In particular, I learned that I have to preserve the ext_inject_handler zero invariant upon branching to the next inject handler. I settled with writing my old inject handler (once) before calling out for EXIT_ALIAS, then handling a nonzero ext_inject_handler that may result either from my old inject or the other alias.eld instance having hooked the inject handler.
    • Add ALIAS STOP and STOPALL commands. These set a flag in the instance that indicates to pop it upon the next time an alias command could run. This mechanism ensures that the EXIT_ALIAS callouts occur in the expected order.

The comlevels mechanism

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 alias buffer format

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 format comment

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.

lineio.asm comment added on 2022-10-14

Review of Debug/X line history patch

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 rawinput is 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

You could leave a comment if you were logged in.
blog/pushbx/2026/0823_mid_late_august_work_multi-aliases.txt · Last modified: 2026-08-23 18:10:37 +0200 Aug Sun by ecm