LOCS: My drawing language developed at age 9

LOCS in action
The Logo language created by Seymour Papert in 1967 was pretty famous in the eighties as a tool to teach programming to kids. Now in 2024, I can see it was great to make nice drawings on the screen and learn trigonometry, and some very fortunate kids had a real robotic turtle drawing on paper, but it wasn't so good for teaching programming because few home computers came with Logo, it was expensive, and its implementations differed greatly in their support of important programming constructs (KingOfCoders informs me that the Amstrad CPC464 (1984) computers came with Dr. Logo, nice!)
For example, there is at least one Logo language for Commodore 64 that implemented support for sprites, but it wasn’t portable to other versions. I learned this the hard way as I wanted to play a “Hunt the Dragon” game made for C64 Logo published in the Mi computer magazine (the whole volume contains Logo tutorials that I used to learn Logo), but Dr. Logo just kept throwing me error messages that I didn’t understand.
Anyway, I was in love with Logo, and being the bold kid I was, I decided to make a Logo language for the student's computer developed by my father, and I would code it in Z80 machine code at age 9. Furthermore, I would have forgotten this completely if it wasn’t because the handwritten program was saved away in the class folder that my father still kept and I found the pages the last Sunday, March 10, 2024, almost 36 years later. I almost couldn't believe it!
These are pictures of the three pages composing the program, and the final page contains the manual! You can see the machine code is somewhat readable because each instruction is neatly separated, and this means I coded directly on the educational computer, and later I copied the machine code listing in my notebook.
LOCS handwritten Z80 machine code, page 1 LOCS handwritten Z80 machine code, page 2 LOCS handwritten Z80 machine code, page 3
I’m glad I put a date on the page: Friday, September 9, 1988. It means it was written after my first Z80 machine code game, so this is my second big machine code program.
LOCS means Lenguaje de Oscar Computadora eStudiantil (or Oscar's language for students' computers). OTEK comes from my father's initials (Oscar Toledo Esteva) which we used as a kind of company name.
My language commands are pretty simple: BORRA clears the screen, TORTUGA centers the "turtle", there are four commands to draw (SM up, AM down, DM right, IM left), and finally we have the PT command (Pone Tortuga) to place the turtle anywhere on the screen.

Making it to work

This program depends heavily on having a keyboard, so this time I won’t port it to Colecovision to save me time. I could assign a function to each key in the keypad but it would require a big code chunk and I would end up rewriting the program completely.
Anyway, here we go analyzing it. We’ll start by preparing a simple MSX translation layer.

    ;
	; LOCS 
	;
	; by Oscar Toledo G.
	; (c) Copyright 1988 Oscar Toledo G.
	;
	; Creation date: Sep/09/1988. I was age 9.
	;

	;
	; Layer to adapt it to MSX computers.
	;
WRTVDP: EQU $0047       ; Set VDP address HL.
WRTVRM: EQU $004D       ; Write byte A to VRAM address HL.
FILVRM: EQU $0056       ; Fill VRAM address HL with A repeated BC times.
SETWRT: EQU $0053       ; Set write address HL for VDP.
CHGET:  EQU $009F       ; Read character from keyboard into A.

VDP:    EQU $98         ; Base VDP port.

        ; MSX Cartridge header

        ORG $4000
        DB "AB"
        DW START
        DW 0
        DW 0
        DW 0
        DW 0

	;
	; Set a VDP address to write.
	;
L0100:  JP SETWRT

	;
	; Copy immediate data to VRAM.
	;
L0169:  EX (SP),HL

.1:     LD A,(HL)
        OR A
        JR Z,.2
        OUT (VDP),a
        INC HL
        JR .1

.2:     EX (SP),HL
        RET

        ;
        ; Clear the screen.
        ; Set screen buffer to VRAM $3c00.
        ;
L04CC:
        LD BC,$0F02     ; VDP register 2 = $3c00.
        CALL WRTVDP
        LD HL,$3C00     ; Clear the screen.
        LD BC,$0300     ; 32x24 grid.
        LD A,$20        ; Space character.
        JP FILVRM       ; Fill VRAM.

	;
	; Read the keyboard.
	;
L04F5:
        CALL CHGET      ; Read keyboard.
        CP $61          ; Is it lowercase?
        RET C
        CP $7B
        RET NC          ; No, return.
        SUB $20         ; Yes, make it uppercase.
        RET

	;
	; Read a hexadecimal number in HL.
	;
READ_WORD:
        CALL READ_BYTE	; Read a byte.
READ_BYTE:
        CALL READ_HEX	; Read a hexadecimal digit.
READ_HEX:
        CALL L04F5	; Read the keyboard.
        PUSH HL
        LD HL,(L87F0)	; Get cursor address.
        CALL WRTVRM	; Display key on the screen.
        INC HL
        LD (L87F0),HL	; Update cursor address.
        POP HL
        SUB $30		; Convert ASCII 0-9 to 0-9.
        CP $0A
        JR C,.1
        SUB $07		; Convert ASCII A-F to 10-15.
.1:     ADD HL,HL       ; Shift HL to the left 4 bits.
        ADD HL,HL
        ADD HL,HL
        ADD HL,HL
        OR L		; Add hexadecimal digit.
        LD L,A
        RET
This program calls the ROM of the educational computer. It uses five functions:
  1. L0100, Set VDP address.
  2. L0169, Copy a message to VRAM.
  3. L04CC, Clear the screen.
  4. L04F5, Read the keyboard.
  5. RST $10, Read a hexadecimal number into HL (4 digits).
It depends also on the screen grid to be available at the VRAM address $3c00 (the MSX default is $1800) so in my translation layer for L04CC it writes the VDP register 2 to change it.
The program starts like this:

START:
	LD SP,L87F0
	CALL L8114
	CALL L0100
	CALL L0169
	DB "LOCS OTEK",0
    IF 0
	LD HL,$3C41
	CALL L0100
	CALL L0169
	DB "32 BYTES",0
    ELSE
	DB 0,0,0
	DB 0,0,0
	DB 0,0,0
	DB 0,0,0,0
	DB 0,0,0,0
	DB 0
    ENDIF
It sets the stack pointer at the initial address for the 2K RAM students computer, and immediately the code is patched before showing the program title.

L8114:	CALL L8075
	LD HL,$3C01
	RET

L8075:	LD HL,$3D50
	LD (L86F0),HL
	CALL L04CC
	RET
The patch sets the "turtle" position to $3D50 (the center of the screen), this position is saved in the L86F0 address, then it clears the screen calling L04CC, and then loads HL with $3C01 to point to the top left of the screen. Notice it isn't exactly $3C00 but $3c01 because my Sony Trinitron TV of the time "ate" the first column.
It took me probably half an hour to discover what I patched with tons of handwritten zero bytes (NOP instructions), and it was simply an extra message saying "32 BYTES". I think that Dr. Logo showed the total bytes available for the program, and I think that saying 32 bytes was my joke because there isn't such thing as a memory manager.

L8028:
        CALL L81C1
        CALL L807F
        CALL L0169
        DB "GRAFICO",0
        LD HL,$3EE1
        CALL L8063
        LD A,$3E
        OUT (VDP),A
        LD (L86F2),HL
        LD DE,L8700
L8049:
        CALL L04F5
        LD (DE),A
        INC DE
        CP $0D
        JR Z,L8068
        LD HL,(L86F2)
        PUSH AF
        CALL L0100
        POP AF
        OUT (VDP),A
        INC HL
        LD (L86F2),HL
        JP L8049

L8063:
        CALL L0100
        INC HL
        RET

L807F:
        PUSH HL
        LD HL,(L86F0)
        CALL L0100
        LD A,$2A
        OUT (VDP),A
        POP HL
        CALL L0100
        RET

L81C1: 
        LD HL,$0118     ; Redefine the # character
        CALL L0100      ; as a solid block.
        LD B,$08
L81C9:
        LD A,$FF
        OUT (VDP),A
        DJNZ L81C9
L81CF: 
        LD HL,$0918
        CALL L0100
        LD B,$08
L81D7:
        LD A,$FF
        OUT (VDP),A
        DJNZ L81D7
        LD HL,$1118
        CALL L0100
        LD B,$08
L81E5:
        LD A,$FF
        OUT (VDP),A
        DJNZ L81E5
        LD HL,$3EC1	; Point to command area.
        RET
The main loop starts by calling L81C1, obviously a patch, because it ends setting HL to $3EC1 (the command area on the screen), but first it redefines the # character to be a solid block. The redefinition is done in three areas of VRAM because the educational computer was hardcoded to VDP mode 2, but the MSX starts in mode 0, so only the first loop is necessary but I kept it all.
The next call jumps to L807F to draw the "turtle" in the current position. An asterisk, I'm surprised I didn't even try to draw a turtle. At the end of the L807F it calls L0100 to point to the VRAM address for the command area.
Now it shows a "GRAFICO" message (akin to a READY message), and in the next row ($3EE1 in VRAM) it puts a > sign to wait for the keyboard. Notice the L8063 patch because I couldn't fit an INC HL to save the current address in VRAM at L86F2.
There's no cursor, it simply waits for the keyboard and displays the typed key on the screen (L86F2 contains the current address in VRAM), and saves the key also in the RAM buffer located at L8700. I've moved early the CP $0D comparison waiting for the Enter key, so it doesn't show trash in MSX, because in the educational computer that character wasn't defined so when displayed it was invisible.
It doesn't handle the backspace key, so you have to think before typing anything.

On your command

At this point, it starts processing the language commands:

L8068:
        LD A,(L8700)
	CP $42
	JR NZ,L808F
	CALL L04CC
	JP L8028
The first implemented command is "BORRA", but at the time I didn't know how to compare whole words, so it simply checks for the first letter "B" (ASCII $42), calls L04CC and jumps back to L8028.

L808F:
        CP $54
        JR NZ,L80BF
        LD HL,(L86F0)
        CALL L0100
        LD A,$20
        OUT (VDP),A
        LD HL,$3D50
        LD (L86F0),HL
        CALL L0100
        LD A,$2A
        OUT (VDP),A
        CALL L80B0
        JP L8028

L80B0: 
        LD HL,$3EC1
L80B3:
        CALL L0100
        LD B,$40
L80B8:
        LD A,$20
        OUT (VDP),A
        DJNZ L80B8
        RET
The second command is "TORTUGA" (T). It simply gets the current position of the turtle, puts a space character to erase it, centers it again on the screen, and draws it again. Apparently I forgot I had a turtle drawing subroutine at L807F, or that the L8028 jump already draws the turtle.

L80BF:
        CP $53		
        JR NZ,L80E6
        LD A,(L8700+3)
        SUB $30
        LD B,A
L80C9:
        LD HL,(L86F0)
        CALL L0100
        LD A,$23
        OUT (VDP),A
        LD DE,$0020
        SBC HL,DE
        LD (L86F0),HL
        DJNZ L80C9
        CALL L80B0
        CALL L807F
        JP L8028
The third command is "SM" (S). It expects the length in the fourth byte as a digit, so you need to type exactly SM followed by a space and the digit.
It converts the digit to a value between 0-9 and saves it into B as counter. It takes the current position of the turtle and draws a block, displaces the pointer, and repeats. It ends clearing the command area, redrawing the turtle, and jumping back to the main loop.
Notice it never validates the length, zero will draw 256 blocks.
Did I say block? Right, I didn't know anything about pixels, so when you don't have pixels, you have blocks, right? A very bold kid.

L80E6:
        CP $44
        JR NZ,L811B
        LD A,(L8700+3)
        SUB $30
        LD B,A
L80F0:
        LD HL,(L86F0)
        CALL L0100
        LD A,$23
        OUT (VDP),A
        LD DE,$0001
        ADD HL,DE
        NOP
        LD (L86F0),HL
        DJNZ L80F0
        CALL L80B0
        CALL L807F
        JP L8028
The fourth command is "DM" (D). It is exactly the same code, I only changed the displacement offset to go the right.

L811B:
        CP $41
        JR NZ,L8142
        LD A,(L8700+3)
        SUB $30
        LD B,A
L8125:
        LD HL,(L86F0)
        CALL L0100
        LD A,$23
        OUT (VDP),A
        LD DE,$0020
        ADD HL,DE
        NOP
        LD (L86F0),HL
        DJNZ L8125
        CALL L80B0
        CALL L807F
        JP L8028

L8142:
        CP $49
        JR NZ,L8169
        LD A,(L8700+3)
        SUB $30
        LD B,A
L814C:
        LD HL,(L86F0)
        CALL L0100
        LD A,$23
        OUT (VDP),A
        LD DE,$0001
        SBC HL,DE
        LD (L86F0),HL
        DJNZ L814C      
        CALL L80B0 
        CALL L807F
        JP L8028
The fifth command is "AM" (A) for going down, and the sixth command "IM" (I) for going to left. Again, I only changed the displacement offsets. I could have saved tons of bytes by having a generic drawing subroutine where you could provide an offset, but I was still learning, and probably afraid of juggling with registers.

L8169:
        CP $50
        JR NZ,L8194-1
        LD HL,$3EE5
        LD (L87F0),HL
        CALL READ_WORD  ; Modified from RST $10
        LD D,H
        LD E,L
        LD HL,(L86F0)
        CALL L0100
        LD A,$20
        OUT (VDP),A
        LD HL,$3C00
        ADD HL,DE
        LD (L86F0),HL
        CALL L0100
        LD A,$2A
        OUT (VDP),A
        CALL L80B0
        JP L8028
The seventh command is "PT". It sets a position for the internal cursor by reading a hexadecimal word into HL that is saved right away into DE. It deletes the turtle from the screen, and repositions it at the DE position added $3c00 to put it into the right VRAM address. Almost the same code as the TORTUGA command, and again I forgot I had a turtle redraw subroutine.
By the way, there is a tiny bug here. In the machine code I calculated wrongly the relative jump JR NZ and it jumps one byte before the right address, as the opcode $81 is an ADD instruction, it doesn't affect and it works just right ($41 in MSX, LD B,C)

L8194:
        LD HL,$3C01
        CALL L0100
        CALL L0169
        DB "* ERROR *",0
        LD B,$05
L81A9:
        PUSH BC
        LD BC,$0000
L81AD:
        DEC BC
        LD A,B
        OR C
        JR NZ,L81AD
        POP BC
        DJNZ L81A9
        LD HL,$3C00
        CALL L80B3
        CALL L80B0
        JP L8028
Finally, my LOCS program ends with an error message for an unrecognized command. It does a big delay, erases it, and returns to the main loop.
I've made a tiny video showing my LOCS program in action.

Downloads

You can download the original LOCS program (495 bytes), and the MSX adapted version (589 bytes with the translation layer), along with the original assembler code for both.
You can assemble it with my all-new assembler Gasm80 (available from https://github.com/nanochess/gasm80) or with TNIasm v0.45.

Epilogue

To be a complete language it would need variables, expressions, and at least a conditional loop. But doesn't matter, HTML is called a language and doesn't have this!
I didn't understand exactly how angles worked at the time so there are none at all in my program. The use of blocks instead of pixels was crazy, also the lack of validation of the user's typing, but I can understand I was pretty enthusiastic (detecting commands with one single letter!) and I empirically discovered the principle of "it is better to have it working than having nothing".
Over the years I kept working on compilers and interpreters for several languages going from BASIC to Pascal and then C, maybe later I'll talk about this. But it wasn't until 2014 that I made again my own language: IntyBASIC, a dialect of BASIC with dedicated statements for Intellivision consoles, and more recently CVBasic.

Related links

Last modified: Mar/19/2024