Some comprehensive BASIC to start with
There are BASICly three (*1) kinds of statements to handle single keystrokes in various BASICs (*2):
Waiting for a single keyvalue to arrive and returning it.
Checking if a key has been pressed, if yes, it's read and returned, otherwise an empty value is returned
Delivering the actual state of the (decoded) keyboard without any waiting or checking.
Most classic BASICs follow version #1 or #2, as they work with keystroke buffers like when there is a terminal connection. Many homecomputer BASICs, especially of the Microsoft kind follow #2, while mainframe BASICs often follow #1. Usually, but not always, it is called
GET (*3). #3 is common among 'creative' BASICs that support game programming, including Sinclair BASIC.
All three have their reasoning:
Version #1 stops the program run until a keypress is received, thus waiting for it does not need to do any further handling. Convenient on more text and business style application - also, in a mainframe environment, input is sometimes only available in blocking versions
Version #2 is about as low level as any terminal based system can get. It only peeks into the input buffer, but if there is a character, it gets extracted and delivered. This allows more interactivity, but still can only report 'no keypress' or 'this key has been pressed and released' (*4).
Version #3 now is something that can only be accomplished if the keyboard state is in direct access to the BASIC interpreter. It delivers this state each time called. So handling input is much like on Assembly programs. There is no filter about pressing or releasing. This has to be done by the BASIC program. As a result, each and every key (*5) behaves much like a joystick switch. Not only pressing, but also exact timing of press and release can be detected - thus including duration of press.
In practice this translates to three different logics:
Version #1 is the most relaxed, as it always returns with a key pressed.
Version #2 also returns when no key is pressed, so if waiting is required, it needs to be looped around. Of course, other things can be done during that loop before checking the keyboard again.
Version #3 does need edge discrimination (*6) when used, differentiating key press and key release to form a keystroke if this is the goal.
Now for your question
What exactly is the logic of INKEY$ in ZX BASIC,
INKEY$ follows Version #3 and delivers just the actual state. Perfect for timing relevant game programming, but a bit more work for the programmer of more mundane tasks.
In your example it works like this:
10 IF INKEY$ <> "" THEN GO TO 10
This loop waits until no key is pressed by reading the matrix over and over again
20 IF INKEY$ = "" THEN GO TO 20
This loop waits until a key is pressed by reading the matrix over and over again
30 PRINT INKEY$;
Now the matrix gets read one more time and the result is displayed.
and why do these two conditions not produce an inescapable loop?
Because it's always checking the actual state, not some buffer. The logic is the one of an edge discriminator (*6), not so much of an OS call as modern languages offer.
But yes, there is a chance to screw it, if you're able to press and release faster than thee lines are executed may result, depending on your timing, in lost keystrokes, missed keystrokes or wrong returned. Then again, We are talking about a human operation the membrane here. Fat chance :))
A truly secure routine would be a bit slower, a bit more complex and look like this:
10 A$ = INKEY$
20 IF A$ = "" THEN GO TO 10
30 IF INKEY$ <> "" THEN GO TO 30
40 PRINT A$
50 GO TO 10
Now we read the keyvalue only once and store it for later processing. This will always record the first key pressed, no matter if later on others got pressed (before releasing all).
*1 - Make that 6, as some BASICs define
INKEY as numeric, while others (like Sinclair) made it string based.
*2 - It's
INKEY on Sinclair,
GET on Microsoft, and maybe other names on lesser known dialects. I just use
INKEY for the remaining answer to simplify wording. I'm well aware the Sinclair might have chosen INKEY instead of GET to make clear that here a different behaviour is to be expected.
*3 - None of these single key reading operations where part of the original Dartmouth BASIC (as FORTRAN didn't offer anything about keystrokes), thus names are made up by each implementation in their own way.
*4 - Usually it's reported the time it is pressed, but both ways have been used.
*5 - Depending on the keyboard structure and scanning, multiple key combinations may or may not work.
*6 - An edge discriminator in electronics is a circuit to detect when a signal changes, like detecting a rising or falling flank (aka edge), for example in interrupt handling or alinke. Here it describes a procedural way to detect when a condition changes - from not pressed to pressend, and back.