Perceptions

See all articles from SYNC v2 n4

The ZX81 Parser (Part 2)

In the last issue I began a discussion of the ZX81’s parser which is a complex and tricky combination of software techniques. In this issue I will continue the discussion by detailing the main parser routines.

Syntax Checking vs. Execution

When you enter a line into the ZX81, the parser is called twice to evaluate it. The first time is when you hit ENTER after you have just typed a line into the system. This call instructs the parser to check the syntactic correctness of the line entered. If the line is in error, an S cursor is placed in the line at the appropriate position. The parser then returns to the keyboard routine to permit the user to correct his error. The process by which any syntax error is shown to the user is simple: an RST 8 instruction is executed. This instruction is followed by a byte which specifies the “REPORT NUMBER”. The RST 8 begins execution by loading X-PTR with the value of CH-ADD, the system’s interpreter pointer (see the ZX81 BASIC Programming Manual, chapter 28).

If there are no syntax errors, the parser returns, and the line is converted into its internal representation which is shown in Figure 1.

Figure 1: The internal representation of a line in Basic. (Line Number (16 bits), Length of Line (16 bits), Text of Line, ENTER)
Figure 1: The internal representation of a line in Basic. (Line Number (16 bits), Length of Line (16 bits), Text of Line, ENTER)

At this point, one of two actions will occur. If a line number was specified, the internal representation will be stored in the program area. Note that, if the text of the line is null (i.e., there is no text after the line number), and if a line number was given, the old version of the line, if any exists, will be deleted. If no line number was given, the parser will be called again. This time, however, it will execute the line.

Clearly, some method must exist to tell the parser whether it should check-syntax or execute the line passed to it. A bit (bit 7) in the FLAGS system variable is allocated for this use. If the parser is to execute the line, the bit will be set. Otherwise (i.e., syntax-check only), it will be reset. Thus the designation EXEC/SYNTAX. A routine SYNTAX-Z (0DA6) is used to check this bit. This routine will return Z true if just-checking is specified. It returns Z false (i.e., NZ) if execution is to occur.

To clarify the use of this flag, let me give an example. Suppose the parser is passed the following line:

PLOT 5,10 A

The code that will be executed is chosen by the parser. This process is detailed in “The Parser Body” below.

It must be made clear that the process of fetching arguments and checking syntax cannot be thought of as disjointed. In the case of the PLOT command, as is the case with almost all other commands which take arguments, all arguments are passed to Section B on the “calculator stack.” (See “Understanding Floating Point Arithmetic: The CALCULATOR Language,” by Ian Logan, SYNC 2:2.) As we will see in the next section, the routines used to fetch arguments check syntax, implicitly, as they move CH-ADD through the line—or rather the routines used to check syntax and fetch arguments, implicitly, as they move CH-ADD through the line.

The Parser Body

The ZX81’s parser is table-driven. This means that all syntax checking and execution are directed by a series of tables.

The parser code is given in Listing 1. There are two entry points into the parser: LINE-SCAN and LINE-RUN. When called via LINE-SCAN, the parser sets EXEC/SYNTAX to SYNTAX (i.e., 0), and calls E-LINE-NO. This subroutine is used to check the line number specified. If the line number is outside the bounds of a legal line number, a REPORT “C” is issued. If the line number is valid, LINE-SCAN falls through to LINE-RUN.

As you probably know, the ZX81 will accept an expression as the argument to an INPUT statement. The parser is used to evaluate this expression. The first action performed by the parser (entered via LINE-RUN) is to check whether an INPUT statement is being executed. This condition is specified by bit 5 of FLAGX. If an INPUT statement is currently being processed, one of the two actions will occur. If the first character is a STOP, and the EXEC/SYNTAX flag=EXEC, the parser will exit by using RST 8 to signal a REPORT “D”. If the first character is not a STOP, then the parser jumps to INPUT-REP (see Class-2). If an INPUT statement is not being executed, a jump is made to LINE-NULL.

The parser, continuing execution at LINE-NULL, now checks to see if the line is null. If it is, the parser returns immediately. If the line is non-null, the parser checks to be sure that the first character in the line is a command. Once again, if it is not, REPORT “C” will be issued. Assuming that the first character in the line is a command, the parser continues by calculating the address of the command’s entry in the parameter table. This is done by using an offset table (0C29). The offset table and the paramater table are shown in Listings 2 and 3, respectively.

Reviewing the parameter table, you will see that each command entry specifies the format of a particular command. For example, PLOT (0C98) is shown to be comprsised of a CLASS-6 item, a comma (,), and another CLASS-6 item. After finding the currently executing command in the table, the parser falls through into a loop (GET-PARAM), which is responsible for fetching successive parameter-bytes from the command’s entry.

After fetching a byte, GET-PARAM checks its value. If the byte’s value is greater than 0Bh, the GET-PARAM loop jumps to the SEPERATOR routine which checks the character in the line against the specified parameter-byte (i.e., or=, etc). If a match is found to exit, SEPERATOR returns to the beginning of the GET-PARAM loop. If the characters did not match, REPORT “C” is issued via RST 8.

Listing 1: The Parser Code.

P-OFFSET:    .BYTE $8B,$8D,$2D,$7F,$81,$49,$75
.BYTE $5F,$40,$42,$2B,$17,$1F,$37
.BYTE $52,$45,$0F,$6D,$2B,$44,$2D
.BYTE $5A,$3B,$4C,$45,$0D,$52,$5A
.BYTE $4D,$15,$6A

Listing 2: The Offset Table.

P-LET:       .BYTE $01,$14,$02   ;Class 1
; " "
; Class 2
P-GOTO: .BYTE $06,$00 ; Class 6
.BYTE $81,$0E ; Class 0
; Addr-$0E81
P-IF: .BYTE $06,$DE,$05 ; Class 6
.BYTE $AB,$0 ; THEN
; Class 5
; Addr-$0DAB
P-GOSUB: .BYTE $06,$00 ; Class 6
.BYTE $B5,$0E ; Addr-$0EB5
P-STOP: .BYTE $00,$DC,$0C ; Class 0
; Addr-$0CDC
P-RETURN: .BYTE $00,$D8,$0E ; Class 0
; Addr-$0ED8
P-FOR: .BYTE $04,$14,$06 ; Class 4
.BYTE $DF,$06,$05 ; " "
.BYTE $B9,$0D ; Class 6
; TO
; Class 5
; Addr-$0DB9
P-NEXT: .BYTE $04,$00 ; Class 4
.BYTE $2E,$0E ; Class 0
; Addr-$0E2E
P-PRINT: .BYTE $05,$CF,$0A ; Class 5
; Addr-$0ACF
P-INPUT: .BYTE $01,$00 ; Class 1
.BYTE $E9,$0E ; Class 0
; Addr-$0EE9
P-DIM: .BYTE $05,$09,$14 ; Class 5
; Addr-$1409
P-REM: ,BYTE $05,$6A,$0D ; Class 5
; Addr-$0D6A
P-NEW: .BYTE $00,$C3,$03 ; Class 0
; Addr-$03C3
P-RUN: .BYTE $03,$AF,$0E ; Class 3
; Addr-$0EAF
P-LIST: .BYTE $03,$30,$07 ; Class 3
; Addr-$0730
P-POKE: .BYTE $06,$1A,$06 ; Class 6
.BYTE $00,$92,$0E ; ","
; Class 6
; Class 0
; Addr-$0E92
             CALL NZ,STK-FETCH
LD HL,FLAGX
OR (HL)
LD (HL),A
EX DE,HL
SET-STRLN: LD (STRLEN),BC
LD (DEST),HL
REM: RET
CLASS-2: POP BC
LD A,(FLAGS)
INPUT-REP: PUSH AF
CALL SCANNING
POP AF
LD BC,$1321
LD D,(FLAGS)
XOR D
AND $40

If, on the other hand, the parameter-byte’s value is less than 0B, the parser uses another offset table (0D16) to find the address of the class handler. The class handler is then called. When it finishes execution, it will (probably) return to the beginning of the GET-PARAM loop.

Class Handlers

A class handler is a routine used to parse out a particular type of variable (i.e., non-constant) parameter from a line. These items include, but are not limited to, variable names and expressions. As class handlers perform extremely diversified functions, each will be presented separately with a description of the effects of its execution.

CLASS-6

CLASS-6 is the routine used to parse out an integral expression. CLASS-6 uses a ROM routine known as SCANNING which is a general expression parsing routine. Although a discussion of expression evaluation techniques is beyond the scope of this article, it suffices to say that the SCANNING leaves the result of its evaluation on the calculator stack. It also sets bit 6 of FLAGS to indicate whether the value parsed was of numeric or string type. When control is returned to CLASS-6, after its call to SCANNING, it checks bit 6 of FLAGS. If a string argument was specified, then REPORT “C” is given. If a numeric argument was supplied, the CLASS-6 returns.

CLASS-1

CLASS-1 is called to parse out an assignable variable name. This procedure begins by calling LOOK-VARS, a routine to pull a variable name out of the source line. LOOK-VARS returns a pointer to the variable’s record in the VARS file. If the variable is not found, LOOK-VARS creates it, and returns a pointer to the created record. CLASS-1 stores the returned pointer and other pertinent information in system variables for later use. It then returns.

CLASS-4

CLASS-4 is a routine used for a purpose similar to that of CLASS-1. It is used to parse out the variable name specified as the argument to a FOR or NEXT command. It calls LOOK-VARS and checks the type bits returned in the C register. It checks to be sure that both bits 5 and 6 are set in the type byte. If they are not set, REPORT “C” is given. If the variable name was valid (i.e., a single-character numeric scalar), CLASS-4 will jump into the middle of the CLASS-1 routine which will then proceed by storing the information on the variable in the appropriate system variables.

Listing 2 (continued)

             JR   NZ,REPORT-C
BIT 7,D
JR NZ,CLASS-END
JRB CHECK-2
CLASS-4: CALL LOOK-VARS
PUSH AF
LD A,C
OR $9F
INC A
JR NZ,REPORT-C
POP AF
JR CLASS-4-2
CLASS-6: CALL SCANNING
BIT 6,(FLAGS)
RET NZ
REPORT-C: RST 8
.BYTE $0B
NO-TO-STK: JR NZ,CLASS-6
CALL SYNTAX-Z
RET Z
RST 28 ; Call the
calculator
.BYTE $A0 ; Stk-zero
.BYTE $34 ; End-calc
RET
SYNTAX-Z: BIT 7,(FLAGS)
RET
LD HL,C-OFFSET
LD B,$00
ADD HL,BC
LD C,(HL)
ADD HL,BC
PUSH HL
RET
SEPERATOR: RST 18
CP C
JR NZ,REPORT-C2
RST 20
RET
C-OFFSET: .BYTE $17,$25,$53,$0F,$6B,$13,$76
CHECK-END CALL SYNTAX-Z
RET NZ
POP BC
CHECK-2: LD A,(HL)
CP NEWLINE
RET Z
REPORT-C2: JR REPORT-C
CLASS-3: CP NEWLINE
CALL NO-TO-STK
CLASS-0: CP A
CLASS-5: POP BC
CALL Z,CHECK-END
EX DE,HL
LD HL,(T_ADDR)
LD C,(HL)

Listing 2 (continued)

             INC  HL
LD B,(HL)
EX DE,HL
CLASS-END: PUSH BC
RET
CLASS-1: CALL LOOK-VARS
CLASS-4-2: LD (FLAGX),$00
JR NC,SET-STK
SET 1,(FLAGX)
JR NZ,SET-STRLN
REPORT-2: RST 8
.BYTE $01
SET-STK: CALL Z,STK-VAR
BIT 6,(FLAGS)
JR NZ,SET-STRLN
XOR A
CALL SYNTAX-Z
LINE-SCAN: LD (FLAGS),01 ; Set up SYNTAX-
checking only
CALL E-LINE-NO
LINE-RUN: CALL SET-MEM
LD HL,ERR_NR
LD (HL),$FF
LD HL,FLAGX
BIT 5,(HL)
JR Z,LINE-NULL
CP "STOP" ; ie E3 hex
LD A,(HL)
JP NZ,INPUT-REP
CALL SYNTAX-Z
RET Z
RST 8 ; Error "D"
.BYTE $0C
STOP: RST 8 ; Give REPORT "9"
.BYTE $08
LINE-NULL: RST 18
LD B,$00
CP NEWLINE
RET Z
LD C,A
RST 20
LD A,C
SUB $E1
JR C,REPORT-C2
LD C,A
LD HL,P-OFFSET
ADD HL,BC
LD C,(HL)
ADD HL,BC
JR GET-PARAM
SCAN-LOOP: LD HL,(T_ADDR)
GET-PARAM: LD A,(HL)
INC HL
LD (T_ADDR),HL
LD BC,SCAN-LOOP
PUSH BC
LD C,A
CP $0B
JR NC,SEPERATOR

CLASS-2

The CLASS-2 routine is responsible for parsing out the value assigned to a variable (previously specified by CLASS-1) in a LET or INPUT statement. The expression is evaluated by SCANNING. The type of the expression evaluated is then checked against the type of the variable (name) stored by CLASS-1. If the types are not equal, then REPORT “C” is issued. If, on the other hand, the types do mix, a jump is made to the LET routine at 1321h.

CLASS-5

IF, PRINT, LPRINT, FOR, REM, LOAD, DIM, and SAVE all have CLASS-5 as their last parameter. The CLASS-5 byte (05h) is followed by two bytes which form an address. The address specified is that of the handler for that command. The command handler is the routine that actually performs the action specified by the command (i.e., placing a dot on the screen, in the case of a PLOT command).

By reviewing the code for CLASS-5, you will see that, after POPing its return address (probably 0CF4h), its gets the next two bytes from the command’s entry in the parameter table. It combines these bytes and jumps to the address specified by the resulting word.

Before executing the jump, a CALL is made, conditionally (if the Z-flag is set) to CHECK-END. This routine checks to be sure that the next character in the line specifies an end-of-line (i.e., a NEWLINE). If it does not, REPORT “C” is given. If the character is a NEWLINE, a return is made.

CLASS-0

CLASS-0 is simply a CP A instruction. This instruction is commonly used by Z80 programmers to set the Z-flag. After executing this instruction, CLASS-0 falls through to CLASS-5. CLASS-5 will then CALL CHECK-END, as the Z-flag is set.

CLASS-3

CLASS-3 handles the argument specified after a RAND, LIST, LLIST, or RUN command. This element can be either a NEWLINE or an integral expression. If a NEWLINE is given without any expression, a default of 0 is assumed, and pushed onto the calculator stack. If a number was specified, CLASS-6 is CALLed to parse the expression. After pushing the appropriate number onto the calculator stack, CLASS-6 returns to CLASS-3. CLASS-3 then falls through to CLASS-0.

In the next issue I will discuss the command handlers themselves.

Once again, I wish to extend my utmost gratitude to Ian Logan for his hard work in disassembling the 8K ROM. All labels in this article come from the result of his work: The Sinclair ZX81 ROM Disassembly, Part A.

Until next time, same relativistic time period, same non-Euclidian universe.

Listing 3: The Parameter Table.

P-RAND:      .BYTE $03,$6C,$0E   ; Class 3
; Addr-$0E6C
P-LOAD: .BYTE $05,$40,$03 ; Class 5
; Addr-$0340
P-SAVE: .BYTE $05,$F6,$02 ; Class 5
; Addr-$02F6
P-CONT: .BYTE $00,$7C,$0E ; Class 0
; Addr-$0E7C
P-CLEAR: .BYTE $00,$9A,$14 ; Class 0
; Addr-$149A
P-CLS: .BYTE $00,$2A,$0A ; Class 0
; Addr-$0A2A
P-PLOT: .BYTE $06,$1A,$06 ; Class 6
.BYTE $00,$AF,$0B ; ","
; Class 6
; Class 0
; Addr-$0BAF
P-UNPLOT: .BYTE $06,$1A,$06 ; Class 6
.BYTE $00,$AF,$0B ; ","
; Class 6
; Class 0
; Addr-$0BAF
P-SCROLL: .BYTE $00,$0E,$0C ; Class 0
; Addr-$0C0E
P-PAUSE: .BYTE $06,$00,$32 ; Class 6
.BYTE $0F ; Class 0
; Addr-$0F32
P-SLOW: .BYTE $00,$2B,$0F ; Class 0
; Addr-$0F2B
P-FAST: .BYTE $00,$23,$0F ; Class 0
; Addr-$0F23
P-COPY: .BYTE $00,$69,$08 ; Class 0
; Addr-$0869
P-LPRINT: .BYTE $05,$CB,$0A ; Class 5
; Addr-$0ACB
P-LLIST: .BYTE $03,$2C,$07 ; Class 3
; Addr-$072C